diff --git a/src/components/Map.tsx b/src/components/Map.tsx index 736257b..77193b8 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -4,6 +4,7 @@ import "leaflet/dist/leaflet.css"; import "./Map.css"; import L from "leaflet"; import { useRouter } from "next/navigation"; +import { mapMarkerStoryHref } from "../lib/storyUrls"; const customIcon = new L.Icon({ iconUrl: "/marker.svg", @@ -123,53 +124,8 @@ const Map = () => { ); const router = useRouter(); - function handleStoryClick(item: string, category: string) { - if (category === "Huldufólk") { - category = "alfar-og-huldufolk"; - } - if (category === "Helgisögur") { - category = "ur-efra-og-nedra-helgisogur"; - } - if (category === "Draugar") { - category = "draugar"; - } - if (category === "Tröll") { - category = "troll"; - } - if (item === "Geirfuglasker") { - item = "geirfugl"; - } - if (item === "Loðmundarfjörður") { - item = "a-lodmfj"; - } - if (item === "Melstaðarkirkja") { - item = "jonas"; - } - if (item === "Skaftafell") { - item = "einar-sk"; - } - if (item === "Jórukleif") { - item = "jora"; - } - if (item === "Eyvindarmúli") { - item = "gudm-eyv"; - } - if (item === "Rafnkelsstaðir") { - item = "flugan"; - } - if (item === "Snjóholt") { - item = "setta2"; - } - if (item === "Reynisstaðarkirkja") { - item = "reynis"; - } - if (item === "Húnavatnssýsla") { - item = "sat-nafn"; - } - if (item === "Hruni") { - item = "hruna"; - } - router.push(`/stories/${category}/${item}`); + function handleStoryClick(markerTitle: string, mapCategory: string) { + router.push(mapMarkerStoryHref(markerTitle, mapCategory)); } useEffect(() => { diff --git a/src/components/StoriesCard.tsx b/src/components/StoriesCard.tsx index 92a2af4..5c78e98 100644 --- a/src/components/StoriesCard.tsx +++ b/src/components/StoriesCard.tsx @@ -13,6 +13,7 @@ import photo9 from "../../public/resources/hidden people 2.png"; import photo10 from "../../public/resources/MYND5.png"; import photo11 from "../../public/resources/gillitrut.png"; import { useMediaQuery } from "../app/hooks/useMediaQuery"; +import { storyHref } from "../lib/storyUrls"; const shuffleArray = (array: T[]): T[] => { const shuffledArray = [...array]; @@ -23,35 +24,6 @@ const shuffleArray = (array: T[]): T[] => { return shuffledArray; }; -const storyNavigations: Record = { - "Álfadrottning í álögum": "alfa-dr", - "Álfafólkið í Loðmundarfirði": "a-lodmfj", - "Álfakóngurinn í Seley": "seley", - "Ábæjar-Skotta": "skotta3", - "Átján draugar úr Blöndu": "18draug", - "Átján sendingar í senn": "18send", - "Átján Skólabræður": "18skolab", - "Andrarímur og Hallgrímsrímur": "andra", - "Bergþór Bláfellingur": "blafell", - Bakkastaður: "bakka", - "Brytinn í Skálholti": "brytinn", - "Dansinn í Hruna": "hruna", -}; - -const categoryNavigations: Record = { - Allt: "all", - Tröll: "troll", - Draugar: "draugar", - "Álfar og Huldufólk": "alfa", - Helgisögur: "efra", -}; - -function storyHref(storyLink: string, categorySlug: string): string { - const categorySegment = categoryNavigations[categorySlug] || categorySlug; - const storySlug = storyNavigations[storyLink] || storyLink; - return `/stories/${categorySegment}/${storySlug}`; -} - type StoriesCardType = { data: { category: string; @@ -91,7 +63,6 @@ export const StoriesCard = ({ data, categoryName, links }: StoriesCardType) => { useEffect(() => { if (categoryName === "all" && data) { - console.log(links); setCategoryStories(data.stories || []); } else if (!Array.isArray(data) && data.category !== "all") { const catStories = Object.values(data?.stories || {}); @@ -131,7 +102,7 @@ export const StoriesCard = ({ data, categoryName, links }: StoriesCardType) => { height={500} quality={isMobile ? 50 : 95} priority={index === 0} - loading={"eager"} + loading={index === 0 ? "eager" : "lazy"} className="w-full h-auto rounded-lg" aria-hidden /> diff --git a/src/lib/storyUrls.ts b/src/lib/storyUrls.ts new file mode 100644 index 0000000..2234660 --- /dev/null +++ b/src/lib/storyUrls.ts @@ -0,0 +1,67 @@ +/** + * Single place for /stories/[category]/[story] path rules used by the feed and the map. + */ + +const storyTitleToSlug: Record = { + "Álfadrottning í álögum": "alfa-dr", + "Álfafólkið í Loðmundarfirði": "a-lodmfj", + "Álfakóngurinn í Seley": "seley", + "Ábæjar-Skotta": "skotta3", + "Átján draugar úr Blöndu": "18draug", + "Átján sendingar í senn": "18send", + "Átján Skólabræður": "18skolab", + "Andrarímur og Hallgrímsrímur": "andra", + "Bergþór Bláfellingur": "blafell", + Bakkastaður: "bakka", + "Brytinn í Skálholti": "brytinn", + "Dansinn í Hruna": "hruna", +}; + +/** Icelandic UI labels from category chips → URL segment (when not already a slug). */ +const categoryDisplayToSegment: Record = { + Allt: "all", + Tröll: "troll", + Draugar: "draugar", + "Álfar og Huldufólk": "alfa", + Helgisögur: "efra", +}; + +const mapUiCategoryToSegment: Record = { + Huldufólk: "alfar-og-huldufolk", + Helgisögur: "ur-efra-og-nedra-helgisogur", + Draugar: "draugar", + Tröll: "troll", +}; + +const mapMarkerTitleToSlug: Record = { + Geirfuglasker: "geirfugl", + Loðmundarfjörður: "a-lodmfj", + Melstaðarkirkja: "jonas", + Skaftafell: "einar-sk", + Jórukleif: "jora", + Eyvindarmúli: "gudm-eyv", + Rafnkelsstaðir: "flugan", + Snjóholt: "setta2", + Reynisstaðarkirkja: "reynis", + Húnavatnssýsla: "sat-nafn", + Hruni: "hruna", +}; + +export function storyHref(storyLink: string, categoryKey: string): string { + const categorySegment = + categoryDisplayToSegment[categoryKey] || categoryKey; + const storySlug = storyTitleToSlug[storyLink] || storyLink; + return `/stories/${categorySegment}/${storySlug}`; +} + +/** Map popup labels → same paths as the stories feed. */ +export function mapMarkerStoryHref( + markerTitle: string, + mapUiCategory: string +): string { + const categorySegment = + mapUiCategoryToSegment[mapUiCategory] ?? mapUiCategory; + const storySlug = + mapMarkerTitleToSlug[markerTitle] ?? markerTitle; + return storyHref(storySlug, categorySegment); +} diff --git a/src/pages/api/all.js b/src/pages/api/all.js index 1b1dbc1..ccc7d0b 100644 --- a/src/pages/api/all.js +++ b/src/pages/api/all.js @@ -1,8 +1,27 @@ import fetch from "node-fetch"; import { load } from "cheerio"; +/** Source index pages on Netútgáfan (static folklore HTML). */ +const THJOD_SOURCE_URLS = [ + "https://netutgafan.snerpa.is/thjod/troll.htm", + "https://netutgafan.snerpa.is/thjod/draug.htm", + "https://netutgafan.snerpa.is/thjod/alfa.htm", + "https://netutgafan.snerpa.is/thjod/efra.htm", +]; + +/** Longer TTL: list changes rarely; reduces cold-load latency from 4 upstream fetches. */ +const CACHE_TTL_MS = 6 * 60 * 60 * 1000; + let cachedData = null; -let cacheExpiration = Date.now(); +let cacheExpiration = 0; + +function extractCategoryPage(html) { + return load(html).extract({ + category: { selector: "h1" }, + stories: [{ selector: "a" }], + links: [{ selector: "a", value: "href" }], + }); +} export default async function handler(req, res) { if (cachedData && Date.now() < cacheExpiration) { @@ -10,100 +29,11 @@ export default async function handler(req, res) { } try { - const [data1, data2, data3, data4] = await Promise.all([ - fetch("https://netutgafan.snerpa.is/thjod/troll.htm") - .then((res) => res.text()) - .then((html) => { - const json = load(html).extract({ - category: { - selector: "h1", - }, - stories: [ - { - selector: "a", - }, - ], - links: [ - { - selector: "a", - - value: "href", - }, - ], - }); - - return json; - }), - fetch("https://netutgafan.snerpa.is/thjod/draug.htm") - .then((res) => res.text()) - .then((html) => { - const json = load(html).extract({ - category: { - selector: "h1", - }, - stories: [ - { - selector: "a", - }, - ], - links: [ - { - selector: "a", - - value: "href", - }, - ], - }); - - return json; - }), - fetch("https://netutgafan.snerpa.is/thjod/alfa.htm") - .then((res) => res.text()) - .then((html) => { - const json = load(html).extract({ - category: { - selector: "h1", - }, - stories: [ - { - selector: "a", - }, - ], - links: [ - { - selector: "a", - - value: "href", - }, - ], - }); - - return json; - }), - fetch("https://netutgafan.snerpa.is/thjod/efra.htm") - .then((res) => res.text()) - .then((html) => { - const json = load(html).extract({ - category: { - selector: "h1", - }, - stories: [ - { - selector: "a", - }, - ], - links: [ - { - selector: "a", - - value: "href", - }, - ], - }); - - return json; - }), - ]); + const [data1, data2, data3, data4] = await Promise.all( + THJOD_SOURCE_URLS.map((url) => + fetch(url).then((r) => r.text()).then(extractCategoryPage) + ) + ); cachedData = [ { @@ -129,11 +59,13 @@ export default async function handler(req, res) { { category: "alfar-og-huldufolk", stories: data3 }, { category: "ur-efra-og-nedra-helgisogur", stories: data4 }, ]; - cacheExpiration = Date.now() + 1000 * 60 * 5; - console.log("data1", data1); - res.status(200).json(cachedData); + cacheExpiration = Date.now() + CACHE_TTL_MS; + return res.status(200).json(cachedData); } catch (error) { - console.log(error); - res.status(500).json({ message: "Error fetching data" }); + console.error("[api/all]", error); + if (cachedData) { + return res.status(200).json(cachedData); + } + return res.status(500).json({ message: "Error fetching data" }); } }