From 38f1ba69b4647fbb247bd17a9e24625c106572be Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:02:32 +0100 Subject: [PATCH 01/41] return 404 on no data for blogposts/events --- src/app/[locale]/blog/[slug]/page.tsx | 2 ++ src/app/[locale]/event/[slug]/page.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index d896cc2..37126a2 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -41,6 +41,8 @@ export async function generateMetadata({ const { slug } = await params const data = await getBlogPost(slug) + if (!data) notFound() + return generateSeoMetadata({ title: data.title, description: data.excerpt, diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index c73392e..6ae68d7 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -25,6 +25,8 @@ export async function generateMetadata({ const { slug } = await params const data = await getEvent(slug) + if (!data) notFound() + const locale = await getLocale() const t = await getTranslations("metaTags") From 52b8f2b7b7381544172dfac42a1902e47ae46aee Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:03:53 +0100 Subject: [PATCH 02/41] remove redundant layout tags --- src/app/[locale]/blog/[slug]/page.tsx | 6 ++---- src/app/[locale]/event/[slug]/page.tsx | 30 +++++++++++--------------- src/app/[locale]/page.tsx | 6 ++---- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 37126a2..46504a5 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -1,5 +1,3 @@ -import Layout from "@/app/[locale]/layout" - import Footer from "@/components/page-sections/footer" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import Back from "@/components/ui/back" @@ -110,7 +108,7 @@ const BlogPost = async ({ params }: BlogPageParams) => { }) return ( - + <>
@@ -150,7 +148,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
- + ) } diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index 6ae68d7..4504171 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -1,5 +1,3 @@ -import Layout from "@/app/[locale]/layout" - import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import Back from "@/components/ui/back" @@ -78,23 +76,21 @@ const Event = async ({ params }: EventPageParams) => { } return ( - -
- -
-

{data.title}

-
- -

{data.location}

-
-
- -

{formatDate(data.date)}

-
-

{data.description}

+
+ +
+

{data.title}

+
+ +

{data.location}

+
+ +

{formatDate(data.date)}

+
+

{data.description}

- +
) } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 67d2da4..58e0702 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,5 +1,3 @@ -import Layout from "@/app/[locale]/layout" - import About from "@/components/page-sections/about" import BoardMembers from "@/components/page-sections/boardMembers" import Contact from "@/components/page-sections/contact" @@ -23,7 +21,7 @@ export function generateViewport(): Viewport { export default function Home() { return ( - + <> @@ -31,6 +29,6 @@ export default function Home() {
- + ) } From d44fbfa9d732da1e111d6a1bccbc1bdde4c943f2 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:11:53 +0100 Subject: [PATCH 03/41] fix 404 page i18n handling --- src/app/[locale]/[...rest]/page.tsx | 6 +++ src/app/[locale]/layout.tsx | 13 ++++-- src/app/[locale]/not-found.tsx | 9 ++++ src/app/global-not-found.tsx | 46 ------------------- src/app/not-found.tsx | 19 ++++++-- .../page-sections/notFound.module.css} | 0 src/components/page-sections/notFound.tsx | 28 +++++++++++ src/i18n.ts | 19 ++++---- 8 files changed, 80 insertions(+), 60 deletions(-) create mode 100644 src/app/[locale]/[...rest]/page.tsx create mode 100644 src/app/[locale]/not-found.tsx delete mode 100644 src/app/global-not-found.tsx rename src/{app/global-not-found.module.css => components/page-sections/notFound.module.css} (100%) create mode 100644 src/components/page-sections/notFound.tsx diff --git a/src/app/[locale]/[...rest]/page.tsx b/src/app/[locale]/[...rest]/page.tsx new file mode 100644 index 0000000..8aac932 --- /dev/null +++ b/src/app/[locale]/[...rest]/page.tsx @@ -0,0 +1,6 @@ +import { notFound } from "next/navigation" + +// Catch unknown paths below a locale so they render [locale]/not-found.tsx +export default function CatchAll() { + notFound() +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index a33eefe..ff19902 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,17 +1,24 @@ // app/[locale]/layout.tsx -import { NextIntlClientProvider } from "next-intl" -import { getLocale } from "next-intl/server" +import { hasLocale, NextIntlClientProvider } from "next-intl" +import { notFound } from "next/navigation" import LanguageSelector from "@components/ui/languageSelector" import UmamiAnalytics from "@/components/umamiAnalytics" +import { locales } from "@/i18n" import { getAnalyticsConfig } from "@/lib/analyticsConfig" export default async function LocaleLayout({ children, + params, }: { children: React.ReactNode + params: Promise<{ locale: string }> }) { - const locale = (await getLocale()) as "no" | "en" + const { locale } = await params + + // [locale] matches any first path segment, e.g. /favicon.txt or /de + if (!hasLocale(locales, locale)) notFound() + const messages = (await import(`../../../messages/${locale}.json`)).default const analyticsConfig = getAnalyticsConfig() diff --git a/src/app/[locale]/not-found.tsx b/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..d89ef8f --- /dev/null +++ b/src/app/[locale]/not-found.tsx @@ -0,0 +1,9 @@ +import NotFound from "@/components/page-sections/notFound" + +import { getLocale } from "next-intl/server" + +export default async function LocaleNotFound() { + const locale = await getLocale() + + return +} diff --git a/src/app/global-not-found.tsx b/src/app/global-not-found.tsx deleted file mode 100644 index 3aeecbe..0000000 --- a/src/app/global-not-found.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Layout from "@/app/layout" - -import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" - -import { Metadata, Viewport } from "next" -import { getTranslations } from "next-intl/server" -import { useTranslations } from "next-intl" -import Link from "next/link" - -import * as styles from "./global-not-found.module.css" - -export async function generateMetadata(): Promise { - const t = await getTranslations("404") - - return generateSeoMetadata({ - title: t("title"), - description: "Uh oh :(", - }) -} - -export function generateViewport(): Viewport { - return generateSeoViewport() -} - -const NotFoundPage = () => { - const t = useTranslations("404") - - return ( - -
-

{t("title")}

-

- {t("text")}{" "} - - 👀 - -

- - {t("back")} - -
-
- ) -} - -export default NotFoundPage diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index fe0937a..a6b4787 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,5 +1,18 @@ -import { redirect } from "next/navigation" +import NotFound from "@/components/page-sections/notFound" -export default function RootPage() { - redirect("/en/404") +import { defaultLocale } from "@/i18n" + +import { Metadata } from "next" +import { getTranslations } from "next-intl/server" + +export async function generateMetadata(): Promise { + const t = await getTranslations({ locale: defaultLocale, namespace: "404" }) + + return { title: `${t("title")} | UiO Gaming` } +} + +// Rendered when [locale]/layout.tsx rejects an unknown locale (e.g. /de), +// so there is no valid locale to use +export default function RootNotFound() { + return } diff --git a/src/app/global-not-found.module.css b/src/components/page-sections/notFound.module.css similarity index 100% rename from src/app/global-not-found.module.css rename to src/components/page-sections/notFound.module.css diff --git a/src/components/page-sections/notFound.tsx b/src/components/page-sections/notFound.tsx new file mode 100644 index 0000000..7359f80 --- /dev/null +++ b/src/components/page-sections/notFound.tsx @@ -0,0 +1,28 @@ +import { getTranslations } from "next-intl/server" +import Link from "next/link" + +import * as styles from "./notFound.module.css" + +const NotFound = async ({ locale }: { locale: string }) => { + const t = await getTranslations({ locale, namespace: "404" }) + + return ( +
+

{t("title")}

+

+ {t("text")}{" "} + + 👀 + +

+ + {t("back")} + +
+ ) +} + +export default NotFound diff --git a/src/i18n.ts b/src/i18n.ts index 0af1ace..f7e2bad 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,15 +1,18 @@ -import { notFound } from "next/navigation" +import { hasLocale } from "next-intl" import { getRequestConfig } from "next-intl/server" -const locales = ["no", "en"] +export const locales = ["no", "en"] as const +export const defaultLocale = "no" -export default getRequestConfig(async ({ requestLocale }) => { - const locale = await requestLocale - - if (!locale || !locales.includes(locale as "no" | "en")) notFound() +export default getRequestConfig(async ({ locale, requestLocale }) => { + // An explicit locale (e.g. getTranslations({ locale })) takes precedence. + // Fall back to the default instead of throwing, so pages rendered outside + // a valid locale (like the root not-found page) can still be translated + const requested = locale ?? (await requestLocale) + const resolved = hasLocale(locales, requested) ? requested : defaultLocale return { - locale, - messages: (await import(`../messages/${locale}.json`)).default, + locale: resolved, + messages: (await import(`../messages/${resolved}.json`)).default, } }) From 13463d4eeb8f239fcb3368250a644dda04f7c4f5 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:13:58 +0100 Subject: [PATCH 04/41] fetch board member data on data change --- src/app/api/revalidate/route.ts | 6 ++- src/components/page-sections/boardMembers.tsx | 26 +------------ src/lib/sanity.ts | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 4e23c2d..0c71965 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -26,10 +26,12 @@ export async function POST(request: NextRequest) { if (slug?.current) { revalidateTag(`event-${slug.current}`, "max") } + } else if (_type === "author") { + revalidateTag("authors", "max") + // Blog posts show the author's name and image + revalidateTag("blog-posts", "max") } - revalidateTag("home", "max") - return NextResponse.json({ revalidated: true, now: Date.now(), diff --git a/src/components/page-sections/boardMembers.tsx b/src/components/page-sections/boardMembers.tsx index b92e708..3ea5fdf 100644 --- a/src/components/page-sections/boardMembers.tsx +++ b/src/components/page-sections/boardMembers.tsx @@ -1,5 +1,5 @@ import { urlFor } from "@/lib/imageUrlBuilder" -import { sanityClient } from "@/lib/sanity" +import { getBoardMembers } from "@/lib/sanity" import { getLocale, getTranslations } from "next-intl/server" import Image from "next/image" @@ -31,29 +31,7 @@ const BoardMembers = async () => { const t = await getTranslations("boardMembers") const locale = (await getLocale()) as "no" | "en" - const query = ` - *[_type == "author" && boardMember == true] | order(orderRank asc) { - _id, - boardMember, - contactInfo, - name, - role { - no, - en - }, - image { - asset->{ - _id, - url - } - }, - bio { - no, - en - } - } - ` - const data: BoardMember[] = await sanityClient.fetch(query) + const data: BoardMember[] = await getBoardMembers() return (
diff --git a/src/lib/sanity.ts b/src/lib/sanity.ts index 17f684c..52559de 100644 --- a/src/lib/sanity.ts +++ b/src/lib/sanity.ts @@ -95,3 +95,40 @@ export async function getAllEvents() { } ) } + +export async function getBoardMembers() { + const query = ` + *[_type == "author" && boardMember == true] | order(orderRank asc) { + _id, + boardMember, + contactInfo, + name, + role { + no, + en + }, + image { + asset->{ + _id, + url + } + }, + bio { + no, + en + } + } + ` + + return sanityClient.fetch( + query, + {}, + { + cache: "force-cache", + next: { + tags: ["authors"], + revalidate: 3600, // 1 hour + }, + } + ) +} From 30da54c78e83484e6bb2705d117c4ec0f66d0f34 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:16:08 +0100 Subject: [PATCH 05/41] use next cache instead of sanity's cache --- src/lib/sanity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/sanity.ts b/src/lib/sanity.ts index 52559de..92e65ad 100644 --- a/src/lib/sanity.ts +++ b/src/lib/sanity.ts @@ -4,7 +4,7 @@ export const sanityClient = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, apiVersion: "2025-02-19", - useCdn: true, + useCdn: false, }) export async function getBlogPost(slug: string) { From a8b31a537994572aa426a914d8f0a6a184b63532 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:18:07 +0100 Subject: [PATCH 06/41] Expire cache tags immediately on webhook revalidation The "max" profile serves stale content once more while revalidating, so the first visit after publishing showed the old version. Co-Authored-By: Claude Opus 5.5 --- src/app/api/revalidate/route.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 0c71965..d9404c0 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -1,6 +1,10 @@ import { revalidateTag } from "next/cache" import { NextRequest, NextResponse } from "next/server" +// Expire immediately so the first visit after a publish gets fresh content. +// The "max" profile would serve the stale version once more while refetching +const expireTag = (tag: string) => revalidateTag(tag, { expire: 0 }) + export async function POST(request: NextRequest) { const secret = request.nextUrl.searchParams.get("secret") @@ -17,19 +21,19 @@ export async function POST(request: NextRequest) { ) if (_type === "post") { - revalidateTag("blog-posts", "max") + expireTag("blog-posts") if (slug?.current) { - revalidateTag(`post-${slug.current}`, "max") + expireTag(`post-${slug.current}`) } } else if (_type === "event") { - revalidateTag("events", "max") + expireTag("events") if (slug?.current) { - revalidateTag(`event-${slug.current}`, "max") + expireTag(`event-${slug.current}`) } } else if (_type === "author") { - revalidateTag("authors", "max") + expireTag("authors") // Blog posts show the author's name and image - revalidateTag("blog-posts", "max") + expireTag("blog-posts") } return NextResponse.json({ From da3d26d16bc5b7419439f7133319bff67a915a3b Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:28:32 +0100 Subject: [PATCH 07/41] Render pages statically using next/root-params getLocale() read request headers, which made every route dynamic and the page-level revalidate a no-op. Make [locale]/layout.tsx the root layout so the locale can be read through next/root-params, set , add generateStaticParams for locales and slugs, and pass the locale explicitly to metadata translations. The middleware now matches all non-file paths so URLs without a locale are redirected instead of hitting the root layout's notFound(). Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/blog/[slug]/page.tsx | 12 ++++--- src/app/[locale]/event/[slug]/page.tsx | 16 +++++---- src/app/[locale]/layout.tsx | 47 +++++++++++++++++++------- src/app/[locale]/page.tsx | 14 ++++++-- src/app/layout.tsx | 26 -------------- src/app/not-found.tsx | 18 ---------- src/components/seo.tsx | 7 ++-- src/i18n.ts | 11 +++--- src/middleware.ts | 5 +-- 9 files changed, 77 insertions(+), 79 deletions(-) delete mode 100644 src/app/layout.tsx delete mode 100644 src/app/not-found.tsx diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 46504a5..998a881 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -8,7 +8,6 @@ import { getBlogPost } from "@/lib/sanity" import { PortableText, PortableTextComponents } from "@portabletext/react" import { Metadata, Viewport } from "next" import { notFound } from "next/navigation" -import { getLocale } from "next-intl/server" import Image from "next/image" import * as styles from "./page.module.css" @@ -33,15 +32,21 @@ interface LinkValue { href: string } +// Render posts on first visit and cache them, instead of on every request +export async function generateStaticParams() { + return [] +} + export async function generateMetadata({ params, }: BlogPageParams): Promise { - const { slug } = await params + const { slug, locale } = await params const data = await getBlogPost(slug) if (!data) notFound() return generateSeoMetadata({ + locale, title: data.title, description: data.excerpt, imageURL: data.mainImage?.asset?.url, @@ -94,8 +99,7 @@ const components: PortableTextComponents = { } const BlogPost = async ({ params }: BlogPageParams) => { - const { slug }: { slug: string } = await params - const locale = await getLocale() + const { slug, locale } = await params const data = await getBlogPost(slug) diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index 4504171..83cafe6 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -5,7 +5,7 @@ import { getEvent } from "@/lib/sanity" import { Metadata, Viewport } from "next" import { notFound } from "next/navigation" -import { getLocale, getTranslations } from "next-intl/server" +import { getTranslations } from "next-intl/server" import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa" import * as styles from "./page.module.css" @@ -17,16 +17,20 @@ interface EventPageParams { }> } +// Render events on first visit and cache them, instead of on every request +export async function generateStaticParams() { + return [] +} + export async function generateMetadata({ params, }: EventPageParams): Promise { - const { slug } = await params + const { slug, locale } = await params const data = await getEvent(slug) if (!data) notFound() - const locale = await getLocale() - const t = await getTranslations("metaTags") + const t = await getTranslations({ locale, namespace: "metaTags" }) const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -43,6 +47,7 @@ export async function generateMetadata({ } return generateSeoMetadata({ + locale, title: data.title, description: `${t("time")}: ${formatDate(data.date)}\n${t("location")}: ${data.location}\n\n${data.description}`, }) @@ -53,8 +58,7 @@ export function generateViewport(): Viewport { } const Event = async ({ params }: EventPageParams) => { - const { slug }: { slug: string } = await params - const locale = await getLocale() + const { slug, locale } = await params const data = await getEvent(slug) diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index ff19902..d514566 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,5 +1,7 @@ -// app/[locale]/layout.tsx +import "@/app/globals.css" + import { hasLocale, NextIntlClientProvider } from "next-intl" +import type { Metadata } from "next" import { notFound } from "next/navigation" import LanguageSelector from "@components/ui/languageSelector" @@ -7,7 +9,22 @@ import UmamiAnalytics from "@/components/umamiAnalytics" import { locales } from "@/i18n" import { getAnalyticsConfig } from "@/lib/analyticsConfig" -export default async function LocaleLayout({ +export const metadata: Metadata = { + title: "UiO Gaming", + description: + "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!", + icons: { + icon: "/logo_2024_neon_pink.png", + shortcut: "/logo_2024_neon_pink.png", + apple: "/logo_2024_neon_pink.png", + }, +} + +export function generateStaticParams() { + return locales.map(locale => ({ locale })) +} + +export default async function RootLayout({ children, params, }: { @@ -24,16 +41,20 @@ export default async function LocaleLayout({ const analyticsConfig = getAnalyticsConfig() return ( - - -
{children}
- {analyticsConfig?.enabled && ( - - )} -
+ + + + +
{children}
+ {analyticsConfig?.enabled && ( + + )} +
+ + ) } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 58e0702..8c89850 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -11,8 +11,18 @@ import { Metadata, Viewport } from "next" export const revalidate = 3600 // 1 hour -export async function generateMetadata(): Promise { - return generateSeoMetadata({}) +interface HomePageParams { + params: Promise<{ + locale: string + }> +} + +export async function generateMetadata({ + params, +}: HomePageParams): Promise { + const { locale } = await params + + return generateSeoMetadata({ locale }) } export function generateViewport(): Viewport { diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index 0635cba..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import "@/app/globals.css" - -import type { Metadata } from "next" - -export const metadata: Metadata = { - title: "UiO Gaming", - description: - "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!", - icons: { - icon: "/logo_2024_neon_pink.png", - shortcut: "/logo_2024_neon_pink.png", - apple: "/logo_2024_neon_pink.png", - }, -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx deleted file mode 100644 index a6b4787..0000000 --- a/src/app/not-found.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import NotFound from "@/components/page-sections/notFound" - -import { defaultLocale } from "@/i18n" - -import { Metadata } from "next" -import { getTranslations } from "next-intl/server" - -export async function generateMetadata(): Promise { - const t = await getTranslations({ locale: defaultLocale, namespace: "404" }) - - return { title: `${t("title")} | UiO Gaming` } -} - -// Rendered when [locale]/layout.tsx rejects an unknown locale (e.g. /de), -// so there is no valid locale to use -export default function RootNotFound() { - return -} diff --git a/src/components/seo.tsx b/src/components/seo.tsx index 44bfb22..c865384 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -1,7 +1,8 @@ import { Metadata, Viewport } from "next" -import { getLocale, getTranslations } from "next-intl/server" +import { getTranslations } from "next-intl/server" type SeoProps = { + locale: string title?: string description?: string imageURL?: string @@ -9,13 +10,13 @@ type SeoProps = { } export async function generateSeoMetadata({ + locale, title, description, imageURL, author, }: SeoProps): Promise { - const t = await getTranslations("metaTags") - const locale = await getLocale() + const t = await getTranslations({ locale, namespace: "metaTags" }) const defaultTitle = "UiO Gaming" const metaDescription = description || t("description") diff --git a/src/i18n.ts b/src/i18n.ts index f7e2bad..50b056c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,14 +1,15 @@ import { hasLocale } from "next-intl" import { getRequestConfig } from "next-intl/server" +import { locale as rootLocale } from "next/root-params" export const locales = ["no", "en"] as const -export const defaultLocale = "no" +const defaultLocale = "no" -export default getRequestConfig(async ({ locale, requestLocale }) => { +export default getRequestConfig(async ({ locale }) => { // An explicit locale (e.g. getTranslations({ locale })) takes precedence. - // Fall back to the default instead of throwing, so pages rendered outside - // a valid locale (like the root not-found page) can still be translated - const requested = locale ?? (await requestLocale) + // The root param can be any first path segment until the layout rejects it, + // so fall back to the default instead of throwing + const requested = locale ?? (await rootLocale()) const resolved = hasLocale(locales, requested) ? requested : defaultLocale return { diff --git a/src/middleware.ts b/src/middleware.ts index 12e7b68..2ce9fdc 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -7,6 +7,7 @@ export default createMiddleware({ }) export const config = { - // Match only internationalized pathnames - matcher: ["/", "/(no|en)/:path*"], + // Match everything except API routes, Next internals and files with an + // extension, so paths without a locale get redirected to one + matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], } From d03278ef0d92a57c9c568733bc8173c97cabfaa1 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:30:12 +0100 Subject: [PATCH 08/41] replace deprecated next lint with eslint --- eslint.config.mjs | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..6d60acc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,9 @@ const compat = new FlatCompat({ }); const eslintConfig = [ + { + ignores: [".next/**", "node_modules/**", "next-env.d.ts"], + }, ...compat.extends("next/core-web-vitals", "next/typescript"), ]; diff --git a/package.json b/package.json index a406f88..c0da613 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start -p 9000", - "lint": "next lint", + "lint": "eslint .", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"" }, "dependencies": { From 1f82345a2f8e9055404a1f0f9370b81327941d0a Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:38:49 +0100 Subject: [PATCH 09/41] Keep .env and local build artifacts out of the Docker image Without a .dockerignore, COPY . . baked the .env file (including the webhook secret) into the image and overwrote the container's node_modules with the host's. Pass public build-time values as build args and provide the rest at runtime through env_file. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 13 +++++++++++++ Dockerfile | 9 ++++++++- docker-compose.yml | 10 +++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..451e761 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Installed and built inside the image +node_modules +.next + +# Secrets are passed in by docker compose, never baked into the image +.env* + +.git +.github +Dockerfile +docker-compose.yml +*.md +npm-debug.log* diff --git a/Dockerfile b/Dockerfile index 5c48030..8d967c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,15 @@ RUN npm install COPY . . +# NEXT_PUBLIC_* values are inlined at build time, and pages are prerendered +# with data from Sanity during the build +ARG NEXT_PUBLIC_SANITY_PROJECT_ID +ARG NEXT_PUBLIC_SANITY_DATASET +ARG NEXT_PUBLIC_UMAMI_SRC +ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID + RUN npm run build EXPOSE 9000 -CMD ["npm", "run", "start"] \ No newline at end of file +CMD ["npm", "run", "start"] diff --git a/docker-compose.yml b/docker-compose.yml index 0759b66..ea6fe06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,15 @@ services: uiogaming.no: container_name: uiogaming.no - build: . + build: + context: . + # Values are read from .env + args: + NEXT_PUBLIC_SANITY_PROJECT_ID: ${NEXT_PUBLIC_SANITY_PROJECT_ID} + NEXT_PUBLIC_SANITY_DATASET: ${NEXT_PUBLIC_SANITY_DATASET} + NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC} + NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID} + env_file: .env ports: - 9000:9000 environment: From b05ecf757dc3e5313a03b76422f3c237e7f93607 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:41:07 +0100 Subject: [PATCH 10/41] Handle events without a description Event cards crashed on a missing description, and the event metadata included "null" for missing fields. Truncate previews by grapheme so emoji aren't split, and show descriptions of exactly 70 characters in full. Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/event/[slug]/page.tsx | 8 +++++- src/components/page-sections/events.tsx | 35 +++++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index 83cafe6..dbf3f44 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -49,7 +49,13 @@ export async function generateMetadata({ return generateSeoMetadata({ locale, title: data.title, - description: `${t("time")}: ${formatDate(data.date)}\n${t("location")}: ${data.location}\n\n${data.description}`, + description: [ + `${t("time")}: ${formatDate(data.date)}`, + data.location && `${t("location")}: ${data.location}`, + data.description && `\n${data.description}`, + ] + .filter(Boolean) + .join("\n"), }) } diff --git a/src/components/page-sections/events.tsx b/src/components/page-sections/events.tsx index ba03020..8512768 100644 --- a/src/components/page-sections/events.tsx +++ b/src/components/page-sections/events.tsx @@ -15,9 +15,11 @@ interface Event { slug: { current: string } - description: string + description: string | null } +const DESCRIPTION_PREVIEW_LENGTH = 70 + const Events = async () => { const t = await getTranslations("events") const tCommon = await getTranslations("common") @@ -47,6 +49,26 @@ const Events = async () => { return date.toLocaleString(locale, options) } + const previewDescription = (description: string | null) => { + if (!description) return null + + // Count graphemes so emoji and accented characters are never cut in half + const graphemes = Array.from(new Intl.Segmenter().segment(description)) + if (graphemes.length <= DESCRIPTION_PREVIEW_LENGTH) return description + + const preview = graphemes + .slice(0, DESCRIPTION_PREVIEW_LENGTH) + .map(({ segment }) => segment) + .join("") + .trimEnd() + + return ( + <> + {preview}... {t("readMore")} + + ) + } + const mapEvents = (events: Event[]) => { if (events.length !== 0) { return events.map((event: Event) => ( @@ -63,16 +85,7 @@ const Events = async () => {

{formatDate(event.date)}

-

- {event.description.length < 70 ? ( - event.description - ) : ( - <> - {event.description.substring(0, 70)}...{" "} - {t("readMore")} - - )} -

+

{previewDescription(event.description)}

)) From c5c366884a2ec6c9099e46d0e8d9a604a6f80768 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:46:06 +0100 Subject: [PATCH 11/41] Fetch only upcoming events from Sanity Filter and limit in GROQ instead of fetching every event and filtering in JavaScript. Event dates are stored as Oslo local time strings, so compare them against the current Oslo time in the same format. Co-Authored-By: Claude Opus 5.5 --- src/components/page-sections/events.tsx | 13 +++---------- src/lib/sanity.ts | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/components/page-sections/events.tsx b/src/components/page-sections/events.tsx index 8512768..ddc5ef9 100644 --- a/src/components/page-sections/events.tsx +++ b/src/components/page-sections/events.tsx @@ -1,4 +1,4 @@ -import { getAllEvents } from "@/lib/sanity" +import { getUpcomingEvents } from "@/lib/sanity" import { getLocale, getTranslations } from "next-intl/server" import Image from "next/image" @@ -25,14 +25,7 @@ const Events = async () => { const tCommon = await getTranslations("common") const locale = await getLocale() - const data: Event[] = await getAllEvents() - - const currentDate = new Date() - const filteredEvents = data - .filter((event: Event) => { - return currentDate <= new Date(event.date) - }) - .slice(0, 2) + const events: Event[] = await getUpcomingEvents(2) const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -105,7 +98,7 @@ const Events = async () => {

{t("title")}

-
{mapEvents(filteredEvents)}
+
{mapEvents(events)}
[type, value]) + ) + const now = `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}` + const query = ` - *[_type == "event"] | order(date asc) { + *[_type == "event" && date >= $now] | order(date asc)[0...$limit] { _id, title, location, @@ -85,7 +103,7 @@ export async function getAllEvents() { return sanityClient.fetch( query, - {}, + { now, limit }, { cache: "force-cache", next: { From c997b74d011c8a72351037a61199c95a933e1111 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:48:49 +0100 Subject: [PATCH 12/41] Verify Sanity webhook signatures in the revalidate route The secret was passed in the query string, which ends up in access logs and doesn't protect the request body. Verify the signed header with @sanity/webhook instead, and return 400 for invalid JSON. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ package-lock.json | 10 ++++++++++ package.json | 1 + src/app/api/revalidate/route.ts | 32 ++++++++++++++++++++++++++++---- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 89597f0..a8eff89 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ MEN Du må spesifisere URL for webhook i Sanity sitt dashbord. URLen dette webhooket skal peke på er `https://uiogaming.no/api/revalidate` med _POST_ forespørselstype. +Sett feltet _Secret_ på webhooket til samme verdi som `SANITY_REVALIDATE_SECRET` i `.env`. Sanity signerer da hver forespørsel, og APIet avviser forespørsler uten gyldig signatur. + Du kan lese mer om det [her](https://www.sanity.io/docs/webhooks). ## Vil du bidra? diff --git a/package-lock.json b/package-lock.json index 7f47a7c..cfac592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@portabletext/react": "^3.2.1", "@sanity/client": "^7.8.1", "@sanity/image-url": "^1.1.0", + "@sanity/webhook": "^4.0.4", "country-flag-icons": "^1.5.19", "install": "^0.13.0", "next": "^16.3.3", @@ -1433,6 +1434,15 @@ "node": ">=10.0.0" } }, + "node_modules/@sanity/webhook": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sanity/webhook/-/webhook-4.0.4.tgz", + "integrity": "sha512-c8LmzR5Wx93zJ10ohhp0XlmPrTNSFyvPcLbNY/sN45Wj5VOngz4FbBMbX9j64sSQLt+676U6OhiVfjs1yw3YCw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@schummar/icu-type-parser": { "version": "1.21.5", "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", diff --git a/package.json b/package.json index c0da613..b264d1f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@portabletext/react": "^3.2.1", "@sanity/client": "^7.8.1", "@sanity/image-url": "^1.1.0", + "@sanity/webhook": "^4.0.4", "country-flag-icons": "^1.5.19", "install": "^0.13.0", "next": "^16.3.3", diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index d9404c0..10a825d 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -1,3 +1,4 @@ +import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook" import { revalidateTag } from "next/cache" import { NextRequest, NextResponse } from "next/server" @@ -6,14 +7,37 @@ import { NextRequest, NextResponse } from "next/server" const expireTag = (tag: string) => revalidateTag(tag, { expire: 0 }) export async function POST(request: NextRequest) { - const secret = request.nextUrl.searchParams.get("secret") + const secret = process.env.SANITY_REVALIDATE_SECRET + if (!secret) { + console.error("SANITY_REVALIDATE_SECRET is not set") + return NextResponse.json( + { message: "Webhook is not configured" }, + { status: 500 } + ) + } - if (secret !== process.env.SANITY_REVALIDATE_SECRET) { - return NextResponse.json({ message: "Invalid secret" }, { status: 401 }) + // Sanity signs the raw body with the webhook secret, so verify it before + // parsing + const rawBody = await request.text() + const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? "" + + // Throws on a malformed signature header instead of returning false + const isValid = await isValidSignature(rawBody, signature, secret).catch( + () => false + ) + + if (!isValid) { + return NextResponse.json({ message: "Invalid signature" }, { status: 401 }) + } + + let body + try { + body = JSON.parse(rawBody) + } catch { + return NextResponse.json({ message: "Invalid JSON body" }, { status: 400 }) } try { - const body = await request.json() const { _type, slug } = body console.log( From d04afb3e0c752c4c33fad8988dd1b45a4e06d5f1 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:50:23 +0100 Subject: [PATCH 13/41] Use real links in the "Other" dropdown The items were buttons calling window.open() without noopener, which let the opened page access window.opener. Links also support middle-click and show the URL on hover. Co-Authored-By: Claude Opus 5.5 --- src/components/ui/otherButton.module.css | 5 +++-- src/components/ui/otherButton.tsx | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/ui/otherButton.module.css b/src/components/ui/otherButton.module.css index f55160c..5b520f5 100644 --- a/src/components/ui/otherButton.module.css +++ b/src/components/ui/otherButton.module.css @@ -36,7 +36,8 @@ border-radius: 10px; } -.dropdownButton { +.dropdownLink { + display: block; width: 100%; color: black; background-color: #cccccc; @@ -52,7 +53,7 @@ padding-right: 2em; } -.dropdownButton:hover { +.dropdownLink:hover { color: black; background-color: #e6e6e6; } diff --git a/src/components/ui/otherButton.tsx b/src/components/ui/otherButton.tsx index e920519..9dd13b6 100644 --- a/src/components/ui/otherButton.tsx +++ b/src/components/ui/otherButton.tsx @@ -36,14 +36,16 @@ const OtherButton = () => { {isOpen && (
{links.map((link, index) => ( - + ))}
)} From a1729494b4a80cc7cae93a66b0659db2fbcc7d6f Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 21:51:53 +0100 Subject: [PATCH 14/41] Only render safe link URLs from blog content Blog links come from the CMS, so a javascript: URL would run code when clicked. Allow only http(s), mailto and relative links, and render anything else as plain text. Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/blog/[slug]/page.tsx | 41 ++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 998a881..a570b3b 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -32,6 +32,22 @@ interface LinkValue { href: string } +const SAFE_LINK_PROTOCOLS = ["http:", "https:", "mailto:"] + +// Links come from the CMS, so reject schemes like javascript: that would run +// code when clicked. Parsing with URL also catches tricks like leading +// whitespace or mixed case that a simple prefix check would miss +const getSafeHref = (href?: string) => { + if (!href) return null + + try { + const { protocol } = new URL(href, "https://uiogaming.no") + return SAFE_LINK_PROTOCOLS.includes(protocol) ? href : null + } catch { + return null + } +} + // Render posts on first visit and cache them, instead of on every request export async function generateStaticParams() { return [] @@ -85,16 +101,21 @@ const components: PortableTextComponents = { }: { children?: React.ReactNode value?: LinkValue - }) => ( - - {children} - - ), + }) => { + const href = getSafeHref(value?.href) + if (!href) return <>{children} + + return ( + + {children} + + ) + }, }, } From aca584791e59a1dc642347cccec8d4c80c3c4311 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:01:56 +0100 Subject: [PATCH 15/41] Update Node to 26 and dependencies to their latest versions - Node 20 is end-of-life; use the current LTS in the Docker image - Major updates: @sanity/client 8, @sanity/image-url 2, @portabletext/react 8, eslint-config-next 16 - Unpin React and update to 19.3 - ESLint stays on 9 and TypeScript on 6.0, since eslint-config-next's plugins and typescript-eslint don't support newer majors yet - Use the native flat config from eslint-config-next 16 - Use useSyncExternalStore for WebGL detection to satisfy the new react-hooks lint rule Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 2 +- eslint.config.mjs | 23 +- package-lock.json | 1488 +++++++++++++++-------- package.json | 33 +- src/components/page-sections/office.tsx | 28 +- src/lib/imageUrlBuilder.ts | 8 +- 6 files changed, 1049 insertions(+), 533 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8d967c6..885ea82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:26-alpine WORKDIR /app diff --git a/eslint.config.mjs b/eslint.config.mjs index 6d60acc..9ae1753 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,19 +1,8 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { defineConfig } from "eslint/config" +import nextVitals from "eslint-config-next/core-web-vitals" +import nextTs from "eslint-config-next/typescript" -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +// eslint-config-next already ignores .next, out, build and next-env.d.ts +const eslintConfig = defineConfig([...nextVitals, ...nextTs]) -const compat = new FlatCompat({ - baseDirectory: __dirname, -}); - -const eslintConfig = [ - { - ignores: [".next/**", "node_modules/**", "next-env.d.ts"], - }, - ...compat.extends("next/core-web-vitals", "next/typescript"), -]; - -export default eslintConfig; +export default eslintConfig diff --git a/package-lock.json b/package-lock.json index cfac592..e042057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,49 +8,324 @@ "name": "uiogaming.no", "version": "1.0.0", "dependencies": { - "@portabletext/react": "^3.2.1", - "@sanity/client": "^7.8.1", - "@sanity/image-url": "^1.1.0", + "@portabletext/react": "^8.0.1", + "@sanity/client": "^8.14.1", + "@sanity/image-url": "^2.1.1", "@sanity/webhook": "^4.0.4", - "country-flag-icons": "^1.5.19", + "country-flag-icons": "^1.6.20", "install": "^0.13.0", - "next": "^16.3.3", - "next-intl": "^4.3.4", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-icons": "^5.5.0", + "next": "^16.4.3", + "next-intl": "^4.14.9", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-icons": "^5.7.0", "react-photo-sphere-viewer": "^6.2.3" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.4.4", - "prettier": "^3.6.2", - "typescript": "^5" + "@types/node": "^26.6.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.3", + "prettier": "^3.9.10", + "typescript": "~6.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", + "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.12", + "@babel/types": "^7.29.11", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.12", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.12", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@eloqnt/config": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@eloqnt/config/-/config-0.0.2.tgz", - "integrity": "sha512-xz86UU1TJQPSzb0XJ3PkoyDlv3wdIiHfuu2wD/8m9ponHx0uSasYRDj4S+dpwQkIQaPYibN1oRr5HBll2GQkog==" + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@eloqnt/config/-/config-0.1.1.tgz", + "integrity": "sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g==", + "license": "MIT" }, "node_modules/@eloqnt/format-json": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@eloqnt/format-json/-/format-json-0.0.3.tgz", - "integrity": "sha512-zgDUVryBYQyM0WZGAoVr+FXjrzRjKkBGJran0E3i5LBcvTycl9ScFVhkF7aRBg8mHrU9xxvclUtSDIYg2mRTQg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eloqnt/format-json/-/format-json-0.1.0.tgz", + "integrity": "sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A==", + "license": "MIT", "dependencies": { - "@eloqnt/config": ">=0.0.2" + "@eloqnt/config": "^0.1.0" } }, "node_modules/@eloqnt/format-po": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@eloqnt/format-po/-/format-po-0.0.3.tgz", - "integrity": "sha512-4r36qEd7KJFV+IMKUv5czNXbGVys8ahxlQKWoUDV20eGl4Oxr+YZ2O5IEQU09S+d7FvPJ1USnyc/wtCLNCo1Ng==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@eloqnt/format-po/-/format-po-0.1.1.tgz", + "integrity": "sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw==", + "license": "MIT", "dependencies": { - "@eloqnt/config": ">=0.0.2", + "@eloqnt/config": "^0.1.1", "po-parser": "^2.2.0" } }, @@ -130,34 +405,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -168,20 +446,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -192,9 +470,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, "license": "MIT", "engines": { @@ -205,9 +483,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -215,13 +493,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -235,18 +513,18 @@ "license": "MIT" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.17.tgz", - "integrity": "sha512-cN9jhVqT7u0K9tix43fhjoUwL0nazyW6zsNIXs2QdPADr+nurPfYyssUiMqcSCGlPcCiqnYVxgSn7zBSuI+5Bg==", + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.22.tgz", + "integrity": "sha512-YuwN+dDKgssfuYwtOyGQD0CSn7K46g9cv1Fp0VgKpjZDDFvv8b8crzP990IcJhRPWHhKzCGsbUSJVIguyyFA5A==", "license": "MIT", "dependencies": { - "@formatjs/icu-skeleton-parser": "2.1.11" + "@formatjs/icu-skeleton-parser": "2.1.12" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", - "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.12.tgz", + "integrity": "sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==", "license": "MIT" }, "node_modules/@formatjs/intl-localematcher": { @@ -825,6 +1103,56 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -839,25 +1167,58 @@ } }, "node_modules/@next/env": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.2.tgz", - "integrity": "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.3.tgz", + "integrity": "sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.4.tgz", - "integrity": "sha512-1FDsyN//ai3Jd97SEd7scw5h1yLdzDACGOPRofr2GD3sEFsBylEEoL0MHSerd4n2dq9Zm/mFMqi4+NRMOreOKA==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.3.tgz", + "integrity": "sha512-oxuC06wt/71h6ACYCV9GbKOfnf5a7VlRe/p80YHTut15TNgDEmynDnaq8t/xhy5rlvk7dhItIPs6H3rvIM4kyw==", "dev": true, "license": "MIT", "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", "fast-glob": "3.3.1" } }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.2.tgz", - "integrity": "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.3.tgz", + "integrity": "sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==", "cpu": [ "arm64" ], @@ -871,9 +1232,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.2.tgz", - "integrity": "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.3.tgz", + "integrity": "sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==", "cpu": [ "x64" ], @@ -887,12 +1248,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.2.tgz", - "integrity": "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.3.tgz", + "integrity": "sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -903,12 +1267,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.2.tgz", - "integrity": "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.3.tgz", + "integrity": "sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -919,12 +1286,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.2.tgz", - "integrity": "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.3.tgz", + "integrity": "sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -935,12 +1305,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.2.tgz", - "integrity": "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.3.tgz", + "integrity": "sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -951,9 +1324,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.2.tgz", - "integrity": "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.3.tgz", + "integrity": "sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==", "cpu": [ "arm64" ], @@ -967,9 +1340,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.2.tgz", - "integrity": "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.3.tgz", + "integrity": "sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==", "cpu": [ "x64" ], @@ -982,6 +1355,27 @@ "node": ">= 10" } }, + "node_modules/@noble/ed25519": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.2.0.tgz", + "integrity": "sha512-criDgRlnUA09hchYrTy/JUWPIEap5rZxQe6wDWzRx51oWWpDRcUpuNzlgPxDJaOK6AsW9c0wKcj3rKRv6t+bPQ==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1348,40 +1742,40 @@ } }, "node_modules/@portabletext/react": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@portabletext/react/-/react-3.2.1.tgz", - "integrity": "sha512-RyFLk6u2q6ZyABTdOk+xoNR2Tq/4fcQFEWayNk4Kbd3gHpUUTabqOrDMChcmG6C7YVLSpwIEBwHoBVcy4vK/hA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@portabletext/react/-/react-8.0.1.tgz", + "integrity": "sha512-Fy9UxVhppgEckEvczZytg4bBxiPcZIM49NrJ3sizEoz19aNzJWEYoUj7f3TI94907psEk0yfR145eoOgkedM2A==", "license": "MIT", "dependencies": { - "@portabletext/toolkit": "^2.0.17", - "@portabletext/types": "^2.0.13" + "@portabletext/toolkit": "^6.0.0", + "@portabletext/types": "^4.0.2" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=22.12" }, "peerDependencies": { - "react": "^17 || ^18 || >=19.0.0-0" + "react": "^19" } }, "node_modules/@portabletext/toolkit": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-2.0.17.tgz", - "integrity": "sha512-5wj+oUaCmHm9Ay1cytPmT1Yc0SrR1twwUIc0qNQ3MtaXaNMPw99Gjt1NcA34yfyKmEf/TAB2NiiT72jFxdddIQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@portabletext/toolkit/-/toolkit-6.0.0.tgz", + "integrity": "sha512-+qSqe0KnefuZfNLUhzQRTpbSKxslZof+3pNwxg0DC8dfZ1yLKyyBUw6iR7EpDL+SJlrnpiBfEcfmEuFitK1NSQ==", "license": "MIT", "dependencies": { - "@portabletext/types": "^2.0.13" + "@portabletext/types": "^4.0.2" }, "engines": { - "node": "^14.13.1 || >=16.0.0" + "node": ">=22.12" } }, "node_modules/@portabletext/types": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-2.0.13.tgz", - "integrity": "sha512-5xk5MSyQU9CrDho3Rsguj38jhijhD36Mk8S6mZo3huv6PM+t4M/5kJN2KFIxgvt4ONpvOEs1pVIZAV0cL0Vi+Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@portabletext/types/-/types-4.0.2.tgz", + "integrity": "sha512-djfIGU9n6DRrunlvj2nIDAp17URo/nA4jSXGvf+Gupx8NLLy9fmJBZ3GL8yhqn9lSVc+cKCharjOa3aOBnWbRw==", "license": "MIT", "engines": { - "node": "^14.13.1 || >=16.0.0 || >=18.0.0" + "node": ">=20.19 <22 || >=22.12" } }, "node_modules/@rtsao/scc": { @@ -1391,47 +1785,41 @@ "dev": true, "license": "MIT" }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", - "dev": true, - "license": "MIT" - }, "node_modules/@sanity/client": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@sanity/client/-/client-7.8.1.tgz", - "integrity": "sha512-jz/IizNHGrss7BG+wFzhI2l2f2avayM4dsBcxKRW4T3HSP4MP7T3oA6vgSDRX1N6KfQfFVILNq6bBE0ut8cHUA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-8.14.1.tgz", + "integrity": "sha512-NQNo0yigdHF4edYwUEVqyDi4ZsTUmWT2YMGUn/sQOW6/T+KPAuVXbVZk6gO+Vzd7dTAhaWj8AV/gIddjpo5P6Q==", "license": "MIT", "dependencies": { - "@sanity/eventsource": "^5.0.2", - "get-it": "^8.6.9", - "nanoid": "^3.3.11", - "rxjs": "^7.0.0" + "eventsource": "^5.1.4", + "get-it": "^9.6.2", + "obug": "^3.0.0", + "rxjs": "^7.8.2" }, "engines": { - "node": ">=20" - } - }, - "node_modules/@sanity/eventsource": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@sanity/eventsource/-/eventsource-5.0.2.tgz", - "integrity": "sha512-/B9PMkUvAlUrpRq0y+NzXgRv5lYCLxZNsBJD2WXVnqZYOfByL9oQBV7KiTaARuObp5hcQYuPfOAVjgXe3hrixA==", - "license": "MIT", - "dependencies": { - "@types/event-source-polyfill": "1.0.5", - "@types/eventsource": "1.1.15", - "event-source-polyfill": "1.0.31", - "eventsource": "2.0.2" + "node": ">=22.12.0" } }, "node_modules/@sanity/image-url": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@sanity/image-url/-/image-url-1.1.0.tgz", - "integrity": "sha512-JHumVRxzzaZAJyOimntdukA9TjjzsJiaiq/uUBdTknMLCNvtM6KQ5OCp6W5fIdY78uyFxtQjz+MPXwK8WBIxWg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sanity/image-url/-/image-url-2.1.1.tgz", + "integrity": "sha512-qVXsF1teWR6FO3ZFAz46HDcRU+E6EQkpyncPnkGOzeJ1PgnaOiWMVbiSrZtX10s3txzzu/tAjKwbxbGHfDWgGw==", "license": "MIT", + "dependencies": { + "@sanity/signed-urls": "^2.0.2" + }, "engines": { - "node": ">=10.0.0" + "node": ">=20.19.0" + } + }, + "node_modules/@sanity/signed-urls": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@sanity/signed-urls/-/signed-urls-2.0.5.tgz", + "integrity": "sha512-kg+LrXyk8tYNuCZnXrSRpTKR6dkbjkfBimGhqUs93VlfhSr56NkbS9N6zXqYRTg2mwbPYd/CAQHNX3SsXJgPcg==", + "license": "MIT", + "dependencies": { + "@noble/ed25519": "^3.1.0", + "@noble/hashes": "^2.3.0" } }, "node_modules/@sanity/webhook": { @@ -1683,27 +2071,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/event-source-polyfill": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz", - "integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==", - "license": "MIT" - }, - "node_modules/@types/eventsource": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", - "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", - "license": "MIT" - }, - "node_modules/@types/follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1719,46 +2086,47 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", - "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "dev": true, + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", - "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.3.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", - "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/type-utils": "8.72.0", - "@typescript-eslint/utils": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1771,15 +2139,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.72.0", + "@typescript-eslint/parser": "^8.72.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { @@ -1787,16 +2155,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", - "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3" }, "engines": { @@ -1812,14 +2180,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", - "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.72.0", - "@typescript-eslint/types": "^8.72.0", + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", "debug": "^4.4.3" }, "engines": { @@ -1834,14 +2202,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", - "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0" + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1852,9 +2220,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", - "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", "dev": true, "license": "MIT", "engines": { @@ -1869,15 +2237,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", - "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0", - "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1894,9 +2262,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", - "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", "dev": true, "license": "MIT", "engines": { @@ -1908,16 +2276,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", - "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.72.0", - "@typescript-eslint/tsconfig-utils": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/visitor-keys": "8.72.0", + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1975,16 +2343,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", - "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.72.0", - "@typescript-eslint/types": "8.72.0", - "@typescript-eslint/typescript-estree": "8.72.0" + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1999,13 +2367,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.72.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", - "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/types": "8.72.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2592,9 +2960,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2627,6 +2995,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2688,9 +3090,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "funding": [ { "type": "opencollective", @@ -2757,10 +3159,17 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/country-flag-icons": { - "version": "1.5.19", - "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.19.tgz", - "integrity": "sha512-D/ZkRyj+ywJC6b2IrAN3/tpbReMUqmuRLlcKFoY/o0+EPQN9Ev/e8tV+D3+9scvu/tarxwLErNwS73C3yzxs/g==", + "version": "1.6.20", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.20.tgz", + "integrity": "sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw==", "license": "MIT" }, "node_modules/cross-spawn": { @@ -2779,10 +3188,10 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2864,21 +3273,6 @@ } } }, - "node_modules/decompress-response": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz", - "integrity": "sha512-6IvPrADQyyPGLpMnUh6kfKiqy7SrbXbjoUuZ90WMBJKErzv2pCiwlGEXjRX9/54OnTq+XFVnkOnOMzclLI5aEA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2959,6 +3353,13 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.469", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.469.tgz", + "integrity": "sha512-HIR230i9MGRHzaB8QRfXePYN23CF5osVvSqrZWQxqmlIMkIrT//wCEiDMM8yK2Il8TTqw39sjbwR4W2/axBFag==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3143,6 +3544,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3157,26 +3568,26 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -3195,7 +3606,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3218,25 +3629,24 @@ } }, "node_modules/eslint-config-next": { - "version": "15.4.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.4.tgz", - "integrity": "sha512-sK/lWLUVF5om18O5w76Jt3F8uzu/LP5mVa6TprCMWkjWHUmByq80iHGHcdH7k1dLiJlj+DRIWf98d5piwRsSuA==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.3.tgz", + "integrity": "sha512-j9IqBma6abp800frdUURaizYMgXOK+jNGyq6CsYcM9AxpSJs5D/TgMNGeAmRUilZvA7g9V1Tr71gNg9/SVTuMQ==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.4.4", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@next/eslint-plugin-next": "16.4.3", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -3245,6 +3655,19 @@ } } }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -3448,16 +3871,23 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react/node_modules/resolve": { @@ -3582,12 +4012,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-source-polyfill": { - "version": "1.0.31", - "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", - "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==", - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -3595,12 +4019,24 @@ "license": "MIT" }, "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-5.1.4.tgz", + "integrity": "sha512-awkjURTcaMUKjtEqM/78nMtFSfGcgGST6EbXGV2/E7LJXzWwnNaByn1zACiX9VluWzokVeHpjRdzo3nZr9zhMg==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^4.1.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/eventsource-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-4.1.3.tgz", + "integrity": "sha512-jZVIVilXZfAx6H20ZKKEj5dabbpD9ETmMj3uHsjMmgBzBHB6tsHrmdWB4mAiy3amtN6MOoVkfXRQu4wf1n88Rw==", "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=22.12" } }, "node_modules/fast-deep-equal": { @@ -3655,9 +4091,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "dev": true, "license": "ISC", "dependencies": { @@ -3728,26 +4164,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", - "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3805,6 +4221,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3831,20 +4257,23 @@ } }, "node_modules/get-it": { - "version": "8.6.10", - "resolved": "https://registry.npmjs.org/get-it/-/get-it-8.6.10.tgz", - "integrity": "sha512-27StIK860ZVp2bhsG/aTWpcoA4OrFxtMqBbesa5sR23m5OxfVQYCnpm2rPQeo3gs5qsUk0FdkISLgXRJ4HynNw==", + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/get-it/-/get-it-9.6.2.tgz", + "integrity": "sha512-eAg+xPVjPXbmFL/ahF08LKmuVVC+ixXCSrb0gtaWOZ9aIsMoa0QATttbtoSoqnVSBV6nh9jCdsOk5pLhN2qUOA==", "license": "MIT", "dependencies": { - "@types/follow-redirects": "^1.14.4", - "decompress-response": "^7.0.0", - "follow-redirects": "^1.15.9", - "is-retry-allowed": "^2.2.0", - "through2": "^4.0.2", - "tunnel-agent": "^0.6.0" + "undici": "^7.30.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=22.12.0" + }, + "peerDependencies": { + "vitest": ">=2.0.0" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } } }, "node_modules/get-proto": { @@ -4042,10 +4471,27 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/icu-minify": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.14.1.tgz", - "integrity": "sha512-I8lz1epVvPmTSCfElYrOVZPMlxXYO6u/HDAd8bAksU0WmL8774lqg/h4PSHIisEYbqYcxXGU2y5b5sbhzCIi0A==", + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.14.9.tgz", + "integrity": "sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw==", "funding": [ { "type": "individual", @@ -4094,12 +4540,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/install": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", @@ -4125,13 +4565,13 @@ } }, "node_modules/intl-messageformat": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.14.tgz", - "integrity": "sha512-9f2VD1HFuxUvMw0RxsaP8WmMns6JRTnsNB/zghTFrp11ZktiXWwVDeZBPQchBKEmo+Gx/ZhxI7Qht7YglFD4PA==", + "version": "11.2.15", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.15.tgz", + "integrity": "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==", "license": "BSD-3-Clause", "dependencies": { "@formatjs/fast-memoize": "3.1.7", - "@formatjs/icu-messageformat-parser": "3.5.17" + "@formatjs/icu-messageformat-parser": "3.5.18" } }, "node_modules/intl-messageformat/node_modules/@formatjs/fast-memoize": { @@ -4140,6 +4580,21 @@ "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", "license": "MIT" }, + "node_modules/intl-messageformat/node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.18.tgz", + "integrity": "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4413,18 +4868,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -4612,6 +5055,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4741,6 +5197,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4775,18 +5241,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -4868,12 +5322,12 @@ } }, "node_modules/next": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz", - "integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.3.tgz", + "integrity": "sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==", "license": "MIT", "dependencies": { - "@next/env": "16.4.2", + "@next/env": "16.4.3", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -4887,14 +5341,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.4.2", - "@next/swc-darwin-x64": "16.4.2", - "@next/swc-linux-arm64-gnu": "16.4.2", - "@next/swc-linux-arm64-musl": "16.4.2", - "@next/swc-linux-x64-gnu": "16.4.2", - "@next/swc-linux-x64-musl": "16.4.2", - "@next/swc-win32-arm64-msvc": "16.4.2", - "@next/swc-win32-x64-msvc": "16.4.2", + "@next/swc-darwin-arm64": "16.4.3", + "@next/swc-darwin-x64": "16.4.3", + "@next/swc-linux-arm64-gnu": "16.4.3", + "@next/swc-linux-arm64-musl": "16.4.3", + "@next/swc-linux-x64-gnu": "16.4.3", + "@next/swc-linux-x64-musl": "16.4.3", + "@next/swc-win32-arm64-msvc": "16.4.3", + "@next/swc-win32-x64-msvc": "16.4.3", "sharp": "^0.35.5" }, "peerDependencies": { @@ -4921,9 +5375,9 @@ } }, "node_modules/next-intl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.14.1.tgz", - "integrity": "sha512-7o7ZyYIWoB0w87n13sezA8r1uLH0ipZCGrMHFKVhwFnwUI1/tym04P/T5NiyULaXUQ276m60xsmInE+HnfAq3A==", + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.14.9.tgz", + "integrity": "sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w==", "funding": [ { "type": "individual", @@ -4932,26 +5386,32 @@ ], "license": "MIT", "dependencies": { - "@eloqnt/config": "^0.0.2", - "@eloqnt/format-json": "^0.0.3", - "@eloqnt/format-po": "^0.0.3", + "@eloqnt/config": "^0.1.1", + "@eloqnt/format-json": "^0.1.0", + "@eloqnt/format-po": "^0.1.1", "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "~1.16.0", - "icu-minify": "^4.14.1", + "icu-minify": "^4.14.9", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "4.14.1", - "use-intl": "^4.14.1" + "next-intl-swc-plugin-extractor": "4.14.9", + "use-intl": "^4.14.9" }, "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.14.1.tgz", - "integrity": "sha512-ixZCS/z1Nx0sto4rWFfoCqahzqXjdhVuQzYrITVwXsrVzYi1ZKrWP8evoXTB5+F+j9FpLQIMIYB6gp3k+zePWA==", + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.14.9.tgz", + "integrity": "sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg==", "license": "MIT" }, "node_modules/next-intl/node_modules/@swc/core": { @@ -5000,6 +5460,16 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-releases": { + "version": "2.0.62", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.62.tgz", + "integrity": "sha512-A0Eq+qVDQI97IbAD6g/HriIuv256el9N8EwF4+xqJ49pwrdRYpdCSmLk0sO4I3fnS6hpYE5v22B73Bk+WArEoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5122,6 +5592,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5304,9 +5787,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", "dev": true, "license": "MIT", "bin": { @@ -5362,30 +5845,30 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.3.1" } }, "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", "license": "MIT", "peerDependencies": { "react": "*" @@ -5416,20 +5899,6 @@ "react-dom": ">=16.0.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5579,26 +6048,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -5635,9 +6084,9 @@ } }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, "node_modules/semver": { @@ -5881,15 +6330,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6082,15 +6522,6 @@ "license": "MIT", "peer": true }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6184,18 +6615,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6288,9 +6707,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6301,6 +6720,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -6320,10 +6763,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6361,6 +6814,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6372,9 +6856,9 @@ } }, "node_modules/use-intl": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.14.1.tgz", - "integrity": "sha512-p6Ggq6AZJDJQpjj4WEjByfid1IEifH7OYpL283y29dzVT2M6vbMoHmYA9KQ/L1A9jHED8uzCNvWic0mJ23kU5A==", + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.14.9.tgz", + "integrity": "sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg==", "funding": [ { "type": "individual", @@ -6385,19 +6869,19 @@ "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "icu-minify": "^4.14.1", + "icu-minify": "^4.14.9", "intl-messageformat": "^11.1.0" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6513,6 +6997,13 @@ "node": ">=0.10.0" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6525,6 +7016,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index b264d1f..29c015e 100644 --- a/package.json +++ b/package.json @@ -12,28 +12,27 @@ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"" }, "dependencies": { - "@portabletext/react": "^3.2.1", - "@sanity/client": "^7.8.1", - "@sanity/image-url": "^1.1.0", + "@portabletext/react": "^8.0.1", + "@sanity/client": "^8.14.1", + "@sanity/image-url": "^2.1.1", "@sanity/webhook": "^4.0.4", - "country-flag-icons": "^1.5.19", + "country-flag-icons": "^1.6.20", "install": "^0.13.0", - "next": "^16.3.3", - "next-intl": "^4.3.4", - "react": "19.1.0", - "react-dom": "19.1.0", - "react-icons": "^5.5.0", + "next": "^16.4.3", + "next-intl": "^4.14.9", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-icons": "^5.7.0", "react-photo-sphere-viewer": "^6.2.3" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.4.4", - "prettier": "^3.6.2", - "typescript": "^5" + "@types/node": "^26.6.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.3", + "prettier": "^3.9.10", + "typescript": "~6.0.3" }, "repository": { "type": "git", diff --git a/src/components/page-sections/office.tsx b/src/components/page-sections/office.tsx index f387208..294795b 100644 --- a/src/components/page-sections/office.tsx +++ b/src/components/page-sections/office.tsx @@ -1,21 +1,33 @@ "use client" -import { useEffect, useState } from "react" +import { useSyncExternalStore } from "react" import { useTranslations } from "next-intl" import Image from "next/image" import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer" import * as styles from "./office.module.css" +let webGL2Support: boolean | undefined + +// Checked once, since creating a WebGL context is not free +const getWebGL2Support = () => { + webGL2Support ??= Boolean( + document.createElement("canvas").getContext("webgl2") + ) + return webGL2Support +} + +// WebGL support never changes, so there is nothing to subscribe to +const subscribe = () => () => {} + const Office = () => { const t = useTranslations("office") - const [supportsWebGL2, setSupportsWebGL2] = useState(null) - - useEffect(() => { - const canvas = document.createElement("canvas") - const gl = canvas.getContext("webgl2") - setSupportsWebGL2(Boolean(gl)) - }, []) + // null on the server, which renders the fallback image + const supportsWebGL2 = useSyncExternalStore( + subscribe, + getWebGL2Support, + () => null + ) return (
diff --git a/src/lib/imageUrlBuilder.ts b/src/lib/imageUrlBuilder.ts index 4b780ae..a800bc8 100644 --- a/src/lib/imageUrlBuilder.ts +++ b/src/lib/imageUrlBuilder.ts @@ -1,9 +1,11 @@ -import imageUrlBuilder from "@sanity/image-url" -import type { SanityImageSource } from "@sanity/image-url/lib/types/types" +import { + createImageUrlBuilder, + type SanityImageSource, +} from "@sanity/image-url" import { sanityClient } from "./sanity" -const builder = imageUrlBuilder(sanityClient) +const builder = createImageUrlBuilder(sanityClient) export function urlFor(source: SanityImageSource) { return builder.image(source) From b84781ebe5c9e07b967a585242e0400129a5baf1 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:10:42 +0100 Subject: [PATCH 16/41] remove unused i18n request config --- src/i18n/request.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/i18n/request.ts diff --git a/src/i18n/request.ts b/src/i18n/request.ts deleted file mode 100644 index f71a330..0000000 --- a/src/i18n/request.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getRequestConfig } from "next-intl/server" -import { cookies } from "next/headers" - -export default getRequestConfig(async () => { - const cookieStore = await cookies() - const locale = cookieStore.get("locale")?.value || "no" - - return { - locale, - messages: (await import(`../../messages/${locale}.json`)).default, - timeZone: "Europe/Oslo", - } -}) From 5b135ce712f02daae4798211d07fffac54a0257a Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:13:57 +0100 Subject: [PATCH 17/41] Define locales once with next-intl's routing config Locales were listed separately in the middleware, the request config and several type casts. Use defineRouting and createNavigation, type the Locale globally, and let the language selector use the locale-aware router so it keeps the query string and hash. Co-Authored-By: Claude Opus 5.5 --- next.config.ts | 2 +- src/app/[locale]/blog/[slug]/page.tsx | 3 ++- src/app/[locale]/event/[slug]/page.tsx | 3 ++- src/app/[locale]/layout.tsx | 6 +++--- src/app/[locale]/page.tsx | 3 ++- src/components/page-sections/boardMembers.tsx | 2 +- src/components/page-sections/notFound.tsx | 3 ++- src/components/seo.tsx | 3 ++- src/components/ui/languageSelector.tsx | 15 +++++++++------ src/i18n/navigation.ts | 7 +++++++ src/{i18n.ts => i18n/request.ts} | 9 +++++---- src/i18n/routing.ts | 14 ++++++++++++++ src/middleware.ts | 8 +++----- 13 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 src/i18n/navigation.ts rename src/{i18n.ts => i18n/request.ts} (69%) create mode 100644 src/i18n/routing.ts diff --git a/next.config.ts b/next.config.ts index 0fdc31e..66e7bc6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next" import createNextIntlPlugin from "next-intl/plugin" -const withNextIntl = createNextIntlPlugin("./src/i18n.ts") +const withNextIntl = createNextIntlPlugin() const nextConfig: NextConfig = { images: { diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index a570b3b..e1593c7 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -7,6 +7,7 @@ import { getBlogPost } from "@/lib/sanity" import { PortableText, PortableTextComponents } from "@portabletext/react" import { Metadata, Viewport } from "next" +import type { Locale } from "next-intl" import { notFound } from "next/navigation" import Image from "next/image" @@ -15,7 +16,7 @@ import * as styles from "./page.module.css" interface BlogPageParams { params: Promise<{ slug: string - locale: string + locale: Locale }> } diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index dbf3f44..03de1fd 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -4,6 +4,7 @@ import Back from "@/components/ui/back" import { getEvent } from "@/lib/sanity" import { Metadata, Viewport } from "next" +import type { Locale } from "next-intl" import { notFound } from "next/navigation" import { getTranslations } from "next-intl/server" import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa" @@ -13,7 +14,7 @@ import * as styles from "./page.module.css" interface EventPageParams { params: Promise<{ slug: string - locale: string + locale: Locale }> } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index d514566..cabe627 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -6,7 +6,7 @@ import { notFound } from "next/navigation" import LanguageSelector from "@components/ui/languageSelector" import UmamiAnalytics from "@/components/umamiAnalytics" -import { locales } from "@/i18n" +import { routing } from "@/i18n/routing" import { getAnalyticsConfig } from "@/lib/analyticsConfig" export const metadata: Metadata = { @@ -21,7 +21,7 @@ export const metadata: Metadata = { } export function generateStaticParams() { - return locales.map(locale => ({ locale })) + return routing.locales.map(locale => ({ locale })) } export default async function RootLayout({ @@ -34,7 +34,7 @@ export default async function RootLayout({ const { locale } = await params // [locale] matches any first path segment, e.g. /favicon.txt or /de - if (!hasLocale(locales, locale)) notFound() + if (!hasLocale(routing.locales, locale)) notFound() const messages = (await import(`../../../messages/${locale}.json`)).default diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 8c89850..7acbd22 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -8,12 +8,13 @@ import Office from "@/components/page-sections/office" import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import { Metadata, Viewport } from "next" +import type { Locale } from "next-intl" export const revalidate = 3600 // 1 hour interface HomePageParams { params: Promise<{ - locale: string + locale: Locale }> } diff --git a/src/components/page-sections/boardMembers.tsx b/src/components/page-sections/boardMembers.tsx index 3ea5fdf..07c767b 100644 --- a/src/components/page-sections/boardMembers.tsx +++ b/src/components/page-sections/boardMembers.tsx @@ -29,7 +29,7 @@ interface BoardMember { const BoardMembers = async () => { const t = await getTranslations("boardMembers") - const locale = (await getLocale()) as "no" | "en" + const locale = await getLocale() const data: BoardMember[] = await getBoardMembers() diff --git a/src/components/page-sections/notFound.tsx b/src/components/page-sections/notFound.tsx index 7359f80..fb42e9c 100644 --- a/src/components/page-sections/notFound.tsx +++ b/src/components/page-sections/notFound.tsx @@ -1,9 +1,10 @@ +import type { Locale } from "next-intl" import { getTranslations } from "next-intl/server" import Link from "next/link" import * as styles from "./notFound.module.css" -const NotFound = async ({ locale }: { locale: string }) => { +const NotFound = async ({ locale }: { locale: Locale }) => { const t = await getTranslations({ locale, namespace: "404" }) return ( diff --git a/src/components/seo.tsx b/src/components/seo.tsx index c865384..2f30c75 100644 --- a/src/components/seo.tsx +++ b/src/components/seo.tsx @@ -1,8 +1,9 @@ import { Metadata, Viewport } from "next" +import type { Locale } from "next-intl" import { getTranslations } from "next-intl/server" type SeoProps = { - locale: string + locale: Locale title?: string description?: string imageURL?: string diff --git a/src/components/ui/languageSelector.tsx b/src/components/ui/languageSelector.tsx index 6212f27..c9634e6 100644 --- a/src/components/ui/languageSelector.tsx +++ b/src/components/ui/languageSelector.tsx @@ -1,20 +1,23 @@ "use client" import { GB, NO } from "country-flag-icons/react/3x2" -import { useLocale } from "next-intl" -import { useRouter, usePathname } from "next/navigation" +import { type Locale, useLocale } from "next-intl" + +import { usePathname, useRouter } from "@/i18n/navigation" import * as styles from "./languageSelector.module.css" const LanguageSelector = () => { const locale = useLocale() const router = useRouter() + // Pathname without the locale prefix const pathname = usePathname() - const changeLocale = (newLocale: "no" | "en") => { - // Remove the current locale from the pathname - const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, "") - router.push(`/${newLocale}${pathWithoutLocale}`) + const changeLocale = (newLocale: Locale) => { + // Read at click time, since useSearchParams() would opt pages out of + // static rendering + const { search, hash } = window.location + router.push(`${pathname}${search}${hash}`, { locale: newLocale }) } return ( diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts new file mode 100644 index 0000000..c6642ca --- /dev/null +++ b/src/i18n/navigation.ts @@ -0,0 +1,7 @@ +import { createNavigation } from "next-intl/navigation" + +import { routing } from "./routing" + +// Locale-aware wrappers around Next's navigation APIs +export const { Link, redirect, usePathname, useRouter, getPathname } = + createNavigation(routing) diff --git a/src/i18n.ts b/src/i18n/request.ts similarity index 69% rename from src/i18n.ts rename to src/i18n/request.ts index 50b056c..79aa093 100644 --- a/src/i18n.ts +++ b/src/i18n/request.ts @@ -2,18 +2,19 @@ import { hasLocale } from "next-intl" import { getRequestConfig } from "next-intl/server" import { locale as rootLocale } from "next/root-params" -export const locales = ["no", "en"] as const -const defaultLocale = "no" +import { routing } from "./routing" export default getRequestConfig(async ({ locale }) => { // An explicit locale (e.g. getTranslations({ locale })) takes precedence. // The root param can be any first path segment until the layout rejects it, // so fall back to the default instead of throwing const requested = locale ?? (await rootLocale()) - const resolved = hasLocale(locales, requested) ? requested : defaultLocale + const resolved = hasLocale(routing.locales, requested) + ? requested + : routing.defaultLocale return { locale: resolved, - messages: (await import(`../messages/${resolved}.json`)).default, + messages: (await import(`../../messages/${resolved}.json`)).default, } }) diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..9cb047d --- /dev/null +++ b/src/i18n/routing.ts @@ -0,0 +1,14 @@ +import { defineRouting } from "next-intl/routing" + +export const routing = defineRouting({ + locales: ["no", "en"], + defaultLocale: "no", + localePrefix: "always", +}) + +// Types getLocale(), useLocale() etc. as "no" | "en" instead of string +declare module "next-intl" { + interface AppConfig { + Locale: (typeof routing.locales)[number] + } +} diff --git a/src/middleware.ts b/src/middleware.ts index 2ce9fdc..cd0ca9e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,8 @@ import createMiddleware from "next-intl/middleware" -export default createMiddleware({ - locales: ["no", "en"], - defaultLocale: "no", - localePrefix: "always", -}) +import { routing } from "./i18n/routing" + +export default createMiddleware(routing) export const config = { // Match everything except API routes, Next internals and files with an From 7c344a2c7498de9f6b310b850e7ae8a2c4d9aa59 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:15:24 +0100 Subject: [PATCH 18/41] Rename middleware.ts to proxy.ts The middleware file convention is deprecated in Next 16. Co-Authored-By: Claude Opus 5.5 --- src/{middleware.ts => proxy.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{middleware.ts => proxy.ts} (100%) diff --git a/src/middleware.ts b/src/proxy.ts similarity index 100% rename from src/middleware.ts rename to src/proxy.ts From 19cda1b817ce75346cf18714f5307e774b6bcd76 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:16:24 +0100 Subject: [PATCH 19/41] Remove accidentally added install package Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 10 ---------- package.json | 1 - 2 files changed, 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index e042057..7b652a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@sanity/image-url": "^2.1.1", "@sanity/webhook": "^4.0.4", "country-flag-icons": "^1.6.20", - "install": "^0.13.0", "next": "^16.4.3", "next-intl": "^4.14.9", "react": "^19.3.1", @@ -4540,15 +4539,6 @@ "node": ">=0.8.19" } }, - "node_modules/install": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", - "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", diff --git a/package.json b/package.json index 29c015e..1fdac74 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@sanity/image-url": "^2.1.1", "@sanity/webhook": "^4.0.4", "country-flag-icons": "^1.6.20", - "install": "^0.13.0", "next": "^16.4.3", "next-intl": "^4.14.9", "react": "^19.3.1", From 35c175c1ad4712ff4c2766bdbcd980e9b0aff036 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:18:02 +0100 Subject: [PATCH 20/41] Remove unused translation key and query field Co-Authored-By: Claude Opus 5.5 --- messages/en.json | 3 +-- messages/no.json | 3 +-- src/components/page-sections/boardMembers.tsx | 1 - src/lib/sanity.ts | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/messages/en.json b/messages/en.json index 35f5b9c..cd40a23 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12,8 +12,7 @@ "description": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO" }, "buttons": { - "back": "back", - "flagImageAlt": "Norwegian flag" + "back": "back" }, "404": { "title": "404: Page not found", diff --git a/messages/no.json b/messages/no.json index c623872..5456102 100644 --- a/messages/no.json +++ b/messages/no.json @@ -12,8 +12,7 @@ "description": "En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!" }, "buttons": { - "back": "tilbake", - "flagImageAlt": "Britisk flagg" + "back": "tilbake" }, "404": { "title": "404: Siden finnes ikke", diff --git a/src/components/page-sections/boardMembers.tsx b/src/components/page-sections/boardMembers.tsx index 07c767b..851df97 100644 --- a/src/components/page-sections/boardMembers.tsx +++ b/src/components/page-sections/boardMembers.tsx @@ -8,7 +8,6 @@ import * as styles from "./boardMembers.module.css" interface BoardMember { _id: string - boardMember: boolean contactInfo?: string name: string role?: { diff --git a/src/lib/sanity.ts b/src/lib/sanity.ts index 833ad9c..d0e4f9e 100644 --- a/src/lib/sanity.ts +++ b/src/lib/sanity.ts @@ -118,7 +118,6 @@ export async function getBoardMembers() { const query = ` *[_type == "author" && boardMember == true] | order(orderRank asc) { _id, - boardMember, contactInfo, name, role { From 5c612d995d6c4c4b6fbcdc1c1366bbd76b5fc92a Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:20:46 +0100 Subject: [PATCH 21/41] Share event date handling and stop depending on the server timezone Event dates are stored as Oslo wall-clock strings, but were parsed in the server's local timezone, so times were only correct because the container sets TZ=Europe/Oslo. Move date parsing and formatting into lib/dates, interpret those strings as Oslo time, and also accept ISO timestamps in case the Studio writes them. Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/event/[slug]/page.tsx | 34 ++----------- src/components/page-sections/events.tsx | 18 +------ src/lib/dates.ts | 67 +++++++++++++++++++++++++ src/lib/sanity.ts | 31 +++++------- 4 files changed, 85 insertions(+), 65 deletions(-) create mode 100644 src/lib/dates.ts diff --git a/src/app/[locale]/event/[slug]/page.tsx b/src/app/[locale]/event/[slug]/page.tsx index 03de1fd..bc1a6d7 100644 --- a/src/app/[locale]/event/[slug]/page.tsx +++ b/src/app/[locale]/event/[slug]/page.tsx @@ -1,6 +1,7 @@ import { generateSeoMetadata, generateSeoViewport } from "@/components/seo" import Back from "@/components/ui/back" +import { formatEventDate } from "@/lib/dates" import { getEvent } from "@/lib/sanity" import { Metadata, Viewport } from "next" @@ -33,25 +34,11 @@ export async function generateMetadata({ const t = await getTranslations({ locale, namespace: "metaTags" }) - const formatDate = (dateString: string) => { - const date = new Date(dateString) - const options: Intl.DateTimeFormatOptions = { - weekday: "long", - month: "long", - year: "numeric", - day: "numeric", - hour: "numeric", - minute: "2-digit", - timeZone: "Europe/Oslo", - } - return date.toLocaleString(locale, options) - } - return generateSeoMetadata({ locale, title: data.title, description: [ - `${t("time")}: ${formatDate(data.date)}`, + `${t("time")}: ${formatEventDate(data.date, locale)}`, data.location && `${t("location")}: ${data.location}`, data.description && `\n${data.description}`, ] @@ -71,21 +58,6 @@ const Event = async ({ params }: EventPageParams) => { if (!data) return notFound() - const formatDate = (dateString: string) => { - const date = new Date(dateString) - const options: Intl.DateTimeFormatOptions = { - weekday: "long", - month: "long", - year: "numeric", - day: "numeric", - hour: "numeric", - minute: "2-digit", - timeZone: "Europe/Oslo", - } - - return date.toLocaleString(locale, options) - } - return (
@@ -97,7 +69,7 @@ const Event = async ({ params }: EventPageParams) => {
-

{formatDate(data.date)}

+

{formatEventDate(data.date, locale)}

{data.description}

diff --git a/src/components/page-sections/events.tsx b/src/components/page-sections/events.tsx index ddc5ef9..bec2296 100644 --- a/src/components/page-sections/events.tsx +++ b/src/components/page-sections/events.tsx @@ -1,3 +1,4 @@ +import { formatEventDate } from "@/lib/dates" import { getUpcomingEvents } from "@/lib/sanity" import { getLocale, getTranslations } from "next-intl/server" @@ -27,21 +28,6 @@ const Events = async () => { const events: Event[] = await getUpcomingEvents(2) - const formatDate = (dateString: string) => { - const date = new Date(dateString) - const options: Intl.DateTimeFormatOptions = { - weekday: "long", - month: "long", - year: "numeric", - day: "numeric", - hour: "numeric", - minute: "2-digit", - timeZone: "Europe/Oslo", - } - - return date.toLocaleString(locale, options) - } - const previewDescription = (description: string | null) => { if (!description) return null @@ -75,7 +61,7 @@ const Events = async () => {
-

{formatDate(event.date)}

+

{formatEventDate(event.date, locale)}

{previewDescription(event.description)}

diff --git a/src/lib/dates.ts b/src/lib/dates.ts new file mode 100644 index 0000000..6d7d3ba --- /dev/null +++ b/src/lib/dates.ts @@ -0,0 +1,67 @@ +import type { Locale } from "next-intl" + +const TIME_ZONE = "Europe/Oslo" + +// Event dates are usually stored as Oslo wall-clock time without an offset +// ("2026-11-05 17:00"). The Studio's datetime field stores ISO timestamps +// instead, so both formats have to be handled +const LOCAL_DATE_FORMAT = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/ + +const osloPartsFormatter = new Intl.DateTimeFormat("en-GB", { + timeZone: TIME_ZONE, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", +}) + +const getOsloParts = (date: Date) => + Object.fromEntries( + osloPartsFormatter + .formatToParts(date) + .map(({ type, value }) => [type, value]) + ) + +/** Formats a date as Oslo wall-clock time, e.g. "2026-11-05 17:00" */ +export const toOsloDateString = (date: Date) => { + const { year, month, day, hour, minute } = getOsloParts(date) + return `${year}-${month}-${day} ${hour}:${minute}` +} + +// How far Oslo's clock is ahead of UTC at the given moment (1 or 2 hours) +const getOsloOffset = (timestamp: number) => { + const { year, month, day, hour, minute } = getOsloParts(new Date(timestamp)) + const osloAsUtc = Date.UTC(+year, +month - 1, +day, +hour, +minute) + return osloAsUtc - Math.floor(timestamp / 60_000) * 60_000 +} + +/** + * Parses an event date. Oslo wall-clock strings are interpreted in Oslo time + * regardless of the server's timezone + */ +export const parseEventDate = (value: string) => { + const match = value.match(LOCAL_DATE_FORMAT) + if (!match) return new Date(value) + + const [year, month, day, hour, minute] = match.slice(1).map(Number) + const wallClockAsUtc = Date.UTC(year, month - 1, day, hour, minute) + + // The offset depends on daylight saving time at the resulting moment, so + // guess with the offset at the wall-clock time, then correct + const guess = wallClockAsUtc - getOsloOffset(wallClockAsUtc) + return new Date(wallClockAsUtc - getOsloOffset(guess)) +} + +/** Formats an event date like "torsdag 5. november 2026 kl. 17:00" */ +export const formatEventDate = (value: string, locale: Locale) => + parseEventDate(value).toLocaleString(locale, { + weekday: "long", + month: "long", + year: "numeric", + day: "numeric", + hour: "numeric", + minute: "2-digit", + timeZone: TIME_ZONE, + }) diff --git a/src/lib/sanity.ts b/src/lib/sanity.ts index d0e4f9e..222b932 100644 --- a/src/lib/sanity.ts +++ b/src/lib/sanity.ts @@ -1,5 +1,7 @@ import { createClient } from "@sanity/client" +import { toOsloDateString } from "./dates" + export const sanityClient = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, @@ -72,26 +74,19 @@ export async function getEvent(slug: string) { } export async function getUpcomingEvents(limit: number) { - // Event dates are stored as Oslo local time strings ("2026-11-05 17:00"), - // which sort correctly as text, so compare against the current time in the - // same format - const parts = Object.fromEntries( - new Intl.DateTimeFormat("en-GB", { - timeZone: "Europe/Oslo", - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hourCycle: "h23", - }) - .formatToParts(new Date()) - .map(({ type, value }) => [type, value]) - ) - const now = `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}` + // Most event dates are Oslo wall-clock strings ("2026-11-05 17:00"), which + // sort correctly as text, so compare them against the current Oslo time in + // the same format. ISO timestamps from the Studio are compared as datetimes + const now = toOsloDateString(new Date()) const query = ` - *[_type == "event" && date >= $now] | order(date asc)[0...$limit] { + *[ + _type == "event" && + select( + defined(dateTime(date)) => dateTime(date) >= dateTime(now()), + date >= $now + ) + ] | order(date asc)[0...$limit] { _id, title, location, From 0eb47846e7b1f98eb37dee6256a20ed715dd49cb Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Fri, 30 Oct 2026 22:23:06 +0100 Subject: [PATCH 22/41] Simplify Umami analytics into one server component The config was split between lib/analyticsConfig and a client component that repeated the production check and default URL, and checked window during render. Umami's data-domains already restricts tracking to our domains. Co-Authored-By: Claude Opus 5.5 --- src/app/[locale]/layout.tsx | 11 +------- src/components/umamiAnalytics.tsx | 46 ++++++------------------------- src/lib/analyticsConfig.ts | 29 ------------------- 3 files changed, 10 insertions(+), 76 deletions(-) delete mode 100644 src/lib/analyticsConfig.ts diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index cabe627..6321337 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -7,7 +7,6 @@ import { notFound } from "next/navigation" import LanguageSelector from "@components/ui/languageSelector" import UmamiAnalytics from "@/components/umamiAnalytics" import { routing } from "@/i18n/routing" -import { getAnalyticsConfig } from "@/lib/analyticsConfig" export const metadata: Metadata = { title: "UiO Gaming", @@ -38,21 +37,13 @@ export default async function RootLayout({ const messages = (await import(`../../../messages/${locale}.json`)).default - const analyticsConfig = getAnalyticsConfig() - return (
{children}
- {analyticsConfig?.enabled && ( - - )} +
diff --git a/src/components/umamiAnalytics.tsx b/src/components/umamiAnalytics.tsx index afc8931..3cd195f 100644 --- a/src/components/umamiAnalytics.tsx +++ b/src/components/umamiAnalytics.tsx @@ -1,49 +1,21 @@ -"use client" - import Script from "next/script" -interface UmamiAnalyticsProps { - websiteId: string - src?: string - domains?: string[] -} +// Umami ignores visits from other domains, so local and preview deployments +// aren't counted +const DOMAINS = ["uiogaming.no", "www.uiogaming.no"] -export default function UmamiAnalytics({ - websiteId, - src = "https://analytics.umami.is/script.js", - domains, -}: UmamiAnalyticsProps) { - if (!websiteId) { - console.warn("Umami: Website ID is required") - return null - } +export default function UmamiAnalytics() { + const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID + const src = + process.env.NEXT_PUBLIC_UMAMI_SRC || "https://analytics.umami.is/script.js" - // Only load analytics in production or if explicitly enabled - if (process.env.NODE_ENV !== "production") { - return null - } - - // Domain validation (client-side check as additional layer) - if (domains && typeof window !== "undefined") { - const currentDomain = window.location.hostname - const isAllowedDomain = domains.some( - domain => currentDomain === domain || currentDomain.endsWith(`.${domain}`) - ) - - if (!isAllowedDomain) { - console.warn( - `Umami: Domain ${currentDomain} not in allowed list:`, - domains - ) - return null - } - } + if (!websiteId || process.env.NODE_ENV !== "production") return null return (