diff --git a/components/custom-image/index.tsx b/components/custom-image/index.tsx index 99982599c..a96fb4ca4 100644 --- a/components/custom-image/index.tsx +++ b/components/custom-image/index.tsx @@ -6,71 +6,99 @@ import noImage from "@/assets/logos/noImage.png"; import { ImageFragment as ImageInterface } from "@/graphql/__generated__/operations"; import { isExternalLink } from "@/utilities"; +/** + * Props for the CustomImage component + * @interface CustomImageProps + */ interface CustomImageProps { + /** The image object containing url, width, height, and alt text */ image: ImageInterface | null; + /** Responsive image sizes attribute */ sizes?: string; + /** Additional CSS classes */ className?: string; + /** Whether to prioritize image loading */ priority?: boolean; + /** Desired image width */ width?: number; + /** Image quality (1-100) */ + quality?: number; } -const isNextStatic = (url: string) => - typeof url != "string" || !url.startsWith("/uploads"); +/** Default dimensions and quality settings */ +const DEFAULT_WIDTH = 384; +const DEFAULT_HEIGHT = 200; +const DEFAULT_QUALITY = 75; +const DEFAULT_SIZES = + "(max-width: 640px) 100vw, (max-width: 1200px) 50vw, 20vw"; +/** + * Checks if the image URL is a Next.js static asset + * @param url - The image URL to check + * @returns boolean indicating if the URL is a Next.js static asset + */ +const isNextStatic = (url: string) => !url.startsWith("/uploads"); + +/** + * Generates the complete image URL with width and quality parameters + * @param imageUrl - The base image URL + * @param width - Desired image width + * @param quality - Desired image quality + * @returns The complete image URL with parameters + */ +const getImageUrl = (imageUrl: string, width: number, quality: number) => { + if (isExternalLink(imageUrl)) return imageUrl; + const baseUrl = env("MEDIA_BASE_URL") || ""; + return `${baseUrl}${imageUrl}?w=${width}&q=${quality}`; +}; + +/** + * A custom Image component that handles various image sources and formats + * Supports external images, Next.js static assets, and dynamic images with optimization + * Falls back to a placeholder image when no image is provided + */ export const CustomImage: FC = ({ image, - sizes, - className, + sizes = DEFAULT_SIZES, + className = "", priority = false, - width, + width = DEFAULT_WIDTH, + quality = DEFAULT_QUALITY, }) => { if (!image) { return ( {"image ); } + const imageProps = { + width: Number(image.width) || DEFAULT_WIDTH, + height: Number(image.height) || DEFAULT_HEIGHT, + className, + alt: image.alt || "", + sizes, + priority, + }; + if (isNextStatic(image.url)) { - return ( - {image.alt - ); + // eslint-disable-next-line jsx-a11y/alt-text + return ; } - const src = isExternalLink(image.url) - ? image.url - : (env("MEDIA_BASE_URL") || "") + image.url; - return ( + // eslint-disable-next-line jsx-a11y/alt-text {image.alt ); }; diff --git a/features/search/search-filters/index.tsx b/features/search/search-filters/index.tsx index 5411be9ad..a535e0efe 100644 --- a/features/search/search-filters/index.tsx +++ b/features/search/search-filters/index.tsx @@ -100,7 +100,7 @@ const MarkAll: FC = ({ search, toggleKey, title }) => { : [], }); - let wildcardFacet: SearchFacetValue = { + const wildcardFacet: SearchFacetValue = { count: -1, facet: toggleKey, facetType: ESType.wildcard, @@ -181,7 +181,7 @@ export const SearchFilters: React.FC = ({ await search.toggleFacet(facetValue); if (search.facetSelected(key, "*")) { - let wildcardFacet: SearchFacetValue = { + const wildcardFacet: SearchFacetValue = { count: -1, facet: key, facetType: ESType.wildcard, diff --git a/features/search/search-input/index.tsx b/features/search/search-input/index.tsx index 804046de8..9f024d663 100644 --- a/features/search/search-input/index.tsx +++ b/features/search/search-input/index.tsx @@ -53,7 +53,7 @@ export const SearchInput: FC = ({ icon={CloseIcon} iconPosition="right" onClick={() => { - submitSearch && submitSearch(""); + submitSearch?.(""); setQuery(""); }} aria-label={t("common|clear-search")} diff --git a/features/search/search-page-selector/index.tsx b/features/search/search-page-selector/index.tsx index 5c17bab98..b17eaeeee 100644 --- a/features/search/search-page-selector/index.tsx +++ b/features/search/search-page-selector/index.tsx @@ -1,80 +1,71 @@ +/** + * @fileoverview Search page selector component that provides navigation tabs + * for different search categories (datasets, concepts, specifications, content). + */ + import { useRouter } from "next/router"; import useTranslation from "next-translate/useTranslation"; import React from "react"; import { ButtonLink } from "@/components/button"; +/** + * Props for the SearchPageSelector component + * @interface + */ interface SearchTabsProps { + /** The current search query string */ query?: string; } -//Navigation between data & Api:s, concepts, specifications. -export const SearchPageSelector: React.FC = ({ query }) => { +/** + * Configuration for search tabs defining their paths and translation keys + * @constant + */ +const SEARCH_TABS = [ + { path: "/datasets", translationKey: "search$datasets" }, + { path: "/concepts", translationKey: "search$concepts" }, + { path: "/specifications", translationKey: "search$specifications" }, + { path: "/search", translationKey: "search$content" }, +] as const; + +/** + * Navigation component that displays tabs for different search categories. + * Highlights the currently active tab and maintains the search query across navigation. + * + * @component + * @param {SearchTabsProps} props - The component props + * @param {string} [props.query] - The current search query string + * @returns {JSX.Element} A navigation component with search category tabs + */ +export function SearchPageSelector({ query }: SearchTabsProps) { const { t, lang } = useTranslation("pages"); const { pathname } = useRouter() || {}; + return ( ); -}; +} diff --git a/utilities/app.ts b/utilities/app.ts index 9def53ace..7cbce3817 100644 --- a/utilities/app.ts +++ b/utilities/app.ts @@ -1,9 +1,11 @@ +import { Translate } from "next-translate"; + import { GoodExampleDataFragment, ImageFragment, NewsItemDataFragment, + SeoDataFragment, } from "@/graphql/__generated__/operations"; -import { SeoDataFragment } from "@/graphql/__generated__/operations"; import { FormResponse, GoodExampleListResponse, @@ -47,7 +49,7 @@ type ResolvedPage = { export const resolvePage = ( props: DataportalPageProps, lang: string, - t: any, + t: Translate, ): ResolvedPage => { if (props.type === "RootAggregate" && lang === "en") { return { diff --git a/utilities/entryscape/blocks/apiexplore.ts b/utilities/entryscape/blocks/apiexplore.ts index 292343e8f..9a05f8de3 100644 --- a/utilities/entryscape/blocks/apiexplore.ts +++ b/utilities/entryscape/blocks/apiexplore.ts @@ -1,6 +1,7 @@ -import { customIndicators } from "./global"; import { Translate } from "next-translate"; +import { customIndicators } from "./global"; + export const apiexploreBlocks = (t: Translate, iconSize: number) => { return [...customIndicators(t, iconSize)]; }; diff --git a/utilities/entryscape/blocks/concept.ts b/utilities/entryscape/blocks/concept.ts index c62b8a582..de691d180 100644 --- a/utilities/entryscape/blocks/concept.ts +++ b/utilities/entryscape/blocks/concept.ts @@ -1,7 +1,9 @@ -import { getSimplifiedLocalizedValue } from "@/utilities/entrystore-utils"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Entry } from "@entryscape/entrystore-js"; import { Translate } from "next-translate"; +import { getSimplifiedLocalizedValue } from "@/utilities/entrystore-utils"; + export const conceptBlocks = (t: Translate, iconSize: number, lang: string) => [ { block: "broaderList", @@ -36,19 +38,19 @@ export const conceptBlocks = (t: Translate, iconSize: number, lang: string) => [ block: "conceptLink", run: function (node: any, a2: any, a3: any, entry: Entry) { if (node && node.firstElementChild && entry) { - var el = document.createElement("a"); + const el = document.createElement("a"); node.setAttribute("class", "entryscape"); node.firstElementChild.appendChild(el); - var ruri = entry.getResourceURI(); - var label = getSimplifiedLocalizedValue( + const ruri = entry.getResourceURI(); + const label = getSimplifiedLocalizedValue( entry.getAllMetadata(), "skos:prefLabel", ); el.innerHTML = label; - var dpUri = getDataportalUri(ruri, lang); + const dpUri = getDataportalUri(ruri, lang); el.setAttribute("href", dpUri); } }, diff --git a/utilities/entryscape/blocks/datasets.ts b/utilities/entryscape/blocks/datasets.ts index 0777ead46..0a4551f4f 100644 --- a/utilities/entryscape/blocks/datasets.ts +++ b/utilities/entryscape/blocks/datasets.ts @@ -1,3 +1,5 @@ +import { Translate } from "next-translate"; + import { catalog, exploreApiLink, @@ -5,7 +7,6 @@ import { theme, customIndicators, } from "./global"; -import { Translate } from "next-translate"; export const datasetBlocks = ( t: Translate, diff --git a/utilities/entryscape/blocks/global.ts b/utilities/entryscape/blocks/global.ts index 20bf06442..f16a14a58 100644 --- a/utilities/entryscape/blocks/global.ts +++ b/utilities/entryscape/blocks/global.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Entry } from "@entryscape/entrystore-js"; import { Translate } from "next-translate"; +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars export const customIndicators = (t: Translate, iconSize: number) => { return [ { @@ -74,9 +76,11 @@ export const customIndicators = (t: Translate, iconSize: number) => { block: "customLicenseIndicator", loadEntry: true, run: function (node: any, data: any, items: any, entry: Entry) { - var v = entry.getAllMetadata().findFirstValue(null, "dcterms:license"); + const v = entry + .getAllMetadata() + .findFirstValue(null, "dcterms:license"); if (v.indexOf("http://creativecommons.org/") === 0) { - var variant; + let variant; if (v === "http://creativecommons.org/publicdomain/zero/1.0/") { variant = "zero"; } else if (v.indexOf("http://creativecommons.org/licenses/") === 0) { @@ -170,12 +174,12 @@ export const exploreApiLink = ( block: "exploreApiLinkRun", run: function (node: any, a2: any, a3: any, entry: Entry) { if (node && node.firstElementChild) { - var showExploreApi = false; - var entryId = entry.getId(); - var contextId = cid; + let showExploreApi = false; + const entryId = entry.getId(); + const contextId = cid; if ((window as any).__es_has_apis) - for (var a in (window as any).__es_has_apis) { + for (const a in (window as any).__es_has_apis) { if ( (window as any).__es_has_apis[a] === contextId + "_" + entryId @@ -183,9 +187,9 @@ export const exploreApiLink = ( showExploreApi = true; } if (showExploreApi) { - var el = document.createElement("a"); - var label = document.createElement("span"); - var svgIcon = document.createElementNS( + const el = document.createElement("a"); + const label = document.createElement("span"); + const svgIcon = document.createElementNS( "http://www.w3.org/2000/svg", "svg", ); @@ -232,8 +236,8 @@ export const hemvist = (t: Translate) => { loadEntry: true, run: function (node: any, data: any, items: any, entry: Entry) { const currentPath = window.location.pathname; - var resourceURI = entry.getResourceURI(); - var linkTitle = t("pages|concept_page$concept_adress"); + const resourceURI = entry.getResourceURI(); + let linkTitle = t("pages|concept_page$concept_adress"); if ( currentPath.includes("/terminology/") || diff --git a/utilities/entryscape/blocks/terminology.ts b/utilities/entryscape/blocks/terminology.ts index a6b611c8b..1e03ef32a 100644 --- a/utilities/entryscape/blocks/terminology.ts +++ b/utilities/entryscape/blocks/terminology.ts @@ -1,25 +1,27 @@ -import { getSimplifiedLocalizedValue } from "@/utilities/entrystore-utils"; +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Entry } from "@entryscape/entrystore-js"; import { Translate } from "next-translate"; +import { getSimplifiedLocalizedValue } from "@/utilities/entrystore-utils"; + export const terminologyBlocks = (t: Translate, lang: string) => [ { block: "conceptLink", run: function (node: any, a2: any, a3: any, entry: Entry) { if (node && node.firstElementChild && entry) { - var el = document.createElement("a"); + const el = document.createElement("a"); node.setAttribute("class", "entryscape"); node.firstElementChild.appendChild(el); - var ruri = entry.getResourceURI(); - var label = getSimplifiedLocalizedValue( + const ruri = entry.getResourceURI(); + const label = getSimplifiedLocalizedValue( entry.getAllMetadata(), "skos:prefLabel", ); el.innerHTML = label; - var dpUri = getDataportalUri(ruri, lang); + const dpUri = getDataportalUri(ruri, lang); el.setAttribute("href", dpUri); } }, diff --git a/utilities/entryscape/entryscape.ts b/utilities/entryscape/entryscape.ts index 0d1791880..6bae689c7 100644 --- a/utilities/entryscape/entryscape.ts +++ b/utilities/entryscape/entryscape.ts @@ -1,6 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { EntryStore, EntryStoreUtil, Entry } from "@entryscape/entrystore-js"; -// @ts-ignore +// @ts-expect-error unknown namespace. import { namespaces } from "@entryscape/rdfjson"; +// @ts-expect-error unknown namespace. +import lucene from "lucene"; import { Translate } from "next-translate"; import { SearchSortOrder } from "@/providers/search-provider"; @@ -27,9 +30,6 @@ import { import { entryCache } from "../local-cache"; -const lucene = require("lucene"); -//const tokenize = require('edge-ngrams')() - //#region ES members /* eslint-disable no-unused-vars */ @@ -140,7 +140,7 @@ export class Entryscape { metaFacets: ESFacetField[], dcat: DCATData, ): Promise<{ [key: string]: SearchFacet }> { - let facets: { [key: string]: SearchFacet } = {}; + const facets: { [key: string]: SearchFacet } = {}; for (const f of metaFacets) { // Find the corresponding facet specification @@ -264,7 +264,7 @@ export class Entryscape { console.error("Error fetching publisher value:", error); } - let themeFacetSpec = this.facetSpecification?.facets?.find( + const themeFacetSpec = this.facetSpecification?.facets?.find( (spec) => spec.resource === "http://www.w3.org/ns/dcat#theme", ); @@ -290,7 +290,7 @@ export class Entryscape { } // Similar approach for formats - let formatFacetSpec = this.facetSpecification?.facets?.find( + const formatFacetSpec = this.facetSpecification?.facets?.find( (spec) => spec.resource === "http://purl.org/dc/terms/format", ); @@ -356,7 +356,7 @@ export class Entryscape { if (q.indexOf('"') === -1) q = q.replace(/ /g, "+AND+"); if (q.length === 0) q = "*"; return q; - } catch (err) { + } catch { // eslint-disable-next-line no-useless-escape return query.replace(/[\!\*\-\+\&\|\(\)\[\]\{\}\^\\~\?\:\"]/g, "").trim(); } @@ -378,7 +378,7 @@ export class Entryscape { const lang = request.language || "sv"; const es = this.getEntryStore(); - let esQuery = es.newSolrQuery(); + const esQuery = es.newSolrQuery(); esQuery.publicRead(true); // Only set up facets if explicitly requested