diff --git a/package-lock.json b/package-lock.json index cf2958e8f..99d6ee282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.6.2", "i18next-resources-to-backend": "^1.2.1", + "js-cookie": "^3.0.5", "jsdom": "^25.0.1", "libphonenumber-js": "^1.11.14", "markdown-to-jsx": "^7.6.2", @@ -9954,10 +9955,13 @@ "license": "BSD-3-Clause" }, "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", - "license": "MIT" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -12547,6 +12551,12 @@ "react-dom": "*" } }, + "node_modules/react-use/node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index a89a4be8e..c417a87bf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "i18next-browser-languagedetector": "^8.0.0", "i18next-http-backend": "^2.6.2", "i18next-resources-to-backend": "^1.2.1", + "js-cookie": "^3.0.5", "jsdom": "^25.0.1", "libphonenumber-js": "^1.11.14", "markdown-to-jsx": "^7.6.2", diff --git a/src/app.tsx b/src/app.tsx index 127effecf..cd207a495 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -17,17 +17,12 @@ import Behov from "./sider/kort/02-behov"; import ArbeidOgFamilie from "./sider/kort/03-arbeid-og-familie"; import {SwitchSoknadType} from "./SwitchSoknadType.tsx"; import Inntekt from "./sider/kort/04-inntekt"; -import {BASE_PATH} from "./lib/constants"; -import {configureLogger} from "@navikt/next-logger"; import "./faro"; import "./lib/i18n/reacti18Next.ts"; -import {initAmplitude} from "./lib/amplitude/Amplitude.tsx"; import {getPathPrefixIncludingLocale} from "./getPathPrefixIncludingLocale.ts"; -import {onLanguageSelect, setParams} from "@navikt/nav-dekoratoren-moduler"; import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; import {ReactQueryDevtools} from "@tanstack/react-query-devtools"; -configureLogger({basePath: BASE_PATH}); const queryClient = new QueryClient(); const RedirectToStandard = () => { @@ -36,8 +31,6 @@ const RedirectToStandard = () => { }; export default function App() { - initAmplitude(); - // @ts-expect-error Polyfill for react-pdf, se https://github.com/wojtekmaj/react-pdf/issues/1831 if (typeof Promise.withResolvers === "undefined") { // @ts-expect-error this is expected to not work @@ -51,17 +44,12 @@ export default function App() { }; } - const [basename, path] = getPathPrefixIncludingLocale(); - - onLanguageSelect(({locale: language, url}) => - setParams({language}).then(() => window.location.assign(`${url}${path}`)) - ); - + const {prefix} = getPathPrefixIncludingLocale(); return ( }> - - - - }> - } /> - } /> - } /> - } /> - } /> - + + + }> + } /> + } /> + } /> + } /> + } /> - - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + + + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/app/[locale]/[...slug]/client.tsx b/src/app/[locale]/[...slug]/client.tsx index d976f5045..45c4f68ca 100644 --- a/src/app/[locale]/[...slug]/client.tsx +++ b/src/app/[locale]/[...slug]/client.tsx @@ -1,10 +1,18 @@ "use client"; -import React from "react"; +import React, {useEffect} from "react"; import dynamic from "next/dynamic"; +import {BASE_PATH} from "../../../lib/constants.ts"; +import {configureLogger} from "@navikt/next-logger"; +import {initAmplitude} from "../../../lib/amplitude/Amplitude.tsx"; const App = dynamic(() => import("../../../app.tsx"), {ssr: false}); export function ClientOnly() { + configureLogger({basePath: BASE_PATH}); + useEffect(() => { + initAmplitude(); + }, []); + return ; } diff --git a/src/app/[locale]/error.tsx b/src/app/[locale]/error.tsx deleted file mode 100644 index 767493591..000000000 --- a/src/app/[locale]/error.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import TekniskFeil from "../../sider/feilsider/TekniskFeil.tsx"; -import {logger} from "@navikt/next-logger"; -import {logError} from "../../lib/log/loggerUtils.ts"; - -export const ErrorComponent = ({error}: {error: Error; reset: () => void}) => { - if (process.env.NEXT_PUBLIC_DIGISOS_ENV !== "localhost") { - logger.error(`En bruker har sett TekniskFeil, error: ${error} referrer: ${document.referrer}`); - logError(`Viser feilside, error, referrer: ${document.referrer}`); - } - - return ; -}; - -export default ErrorComponent; diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index f4b7e6299..94f152600 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,52 +1,26 @@ -import {fetchDecoratorReact} from "@navikt/nav-dekoratoren-moduler/ssr"; -import Script from "next/script"; -import {DECORATOR_SETTINGS} from "../../decoratorSettings.tsx"; -import {Driftsmeldinger} from "../../lib/driftsmeldinger/Driftsmeldinger.tsx"; -import {notFound} from "next/navigation"; import {isSupportedLanguage} from "../../lib/i18n/common.ts"; -import {getMessages} from "next-intl/server"; -import {NextIntlClientProvider} from "next-intl"; -import "../../index.css"; import {DigisosContextProvider} from "../../lib/providers/DigisosContextProvider.tsx"; +import {NextIntlClientProvider} from "next-intl"; +import {getMessages} from "next-intl/server"; -export const dynamic = "force-dynamic"; - -export default async function RootLayout({ +export default async function Layout({ children, params, }: { children: React.ReactNode; params: Promise<{locale: string}>; }) { - const {locale} = await params; - if (!isSupportedLanguage(locale)) notFound(); - - const Decorator = await fetchDecoratorReact({ - ...DECORATOR_SETTINGS, - params: {...DECORATOR_SETTINGS.params, language: locale}, - }); - + const {locale: localeParam} = await params; const messages = await getMessages(); - // locale blir hentet via middleware.ts, - // og html lang leses (som document.documentElement.lang) av både analytics og klientside i18n + const locale = isSupportedLanguage(localeParam) ? localeParam : "nb"; return ( - - - Søknad om økonomisk sosialhjelp - - - - - -
- - {children} - -
- - - - + + {children} + ); } + +export async function generateStaticParams() { + return [{locale: "nb"}, {locale: "nn"}, {locale: "en"}]; +} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index 85684ee45..9675908b1 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,7 +1,25 @@ "use client"; import Informasjon from "../../sider/hovedmeny"; +import {getPathPrefixIncludingLocale} from "../../getPathPrefixIncludingLocale.ts"; +import {onLanguageSelect, setParams} from "@navikt/nav-dekoratoren-moduler"; +import {useContext, useEffect} from "react"; +import {DigisosContext} from "../../lib/providers/DigisosContext.ts"; +import {BASE_PATH} from "../../lib/constants.ts"; +import {configureLogger} from "@navikt/next-logger"; +import {initAmplitude} from "../../lib/amplitude/Amplitude.tsx"; const Page = () => { + configureLogger({basePath: BASE_PATH}); + initAmplitude(); + const {path} = getPathPrefixIncludingLocale(); + const locale = useContext(DigisosContext)!.locale; + useEffect(() => { + document.documentElement.lang = locale; + setParams({language: locale}).then(() => {}); + }, [locale]); + onLanguageSelect(({locale: language, url}) => + setParams({language}).then(() => window.location.assign(`${url}${path}`)) + ); return ; }; diff --git a/src/app/[locale]/api/logger/route.ts b/src/app/api/logger/route.ts similarity index 100% rename from src/app/[locale]/api/logger/route.ts rename to src/app/api/logger/route.ts diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 000000000..f4d64e8e4 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,23 @@ +"use client"; + +import TekniskFeil from "../sider/feilsider/TekniskFeil.tsx"; +import {logger} from "@navikt/next-logger"; +import {logError} from "../lib/log/loggerUtils.ts"; +import {useEffect} from "react"; +import {faro} from "@grafana/faro-react"; + +export const ErrorComponent = ({error, reset}: {error: Error; reset: () => void}) => { + if (faro.api) faro.api.pushError(error); + useEffect(() => { + if (process.env.NEXT_PUBLIC_DIGISOS_ENV === "localhost") { + logger.error( + {errorMessage: error.message, referrer: document.referrer, location: document.location.href}, + `En bruker har sett TekniskFeil` + ); + logError(`Viser feilside, error, referrer: ${document.referrer}`); + } + }, [error]); + return ; +}; + +export default ErrorComponent; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 000000000..e5489c699 --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,39 @@ +"use client"; + +import {configureLogger, logger} from "@navikt/next-logger"; +import Cookie from "js-cookie"; +import {BASE_PATH, DECORATOR_LANG_COOKIE} from "../lib/constants.ts"; +import {isSupportedLanguage} from "../lib/i18n/common.ts"; +import {AbstractIntlMessages, NextIntlClientProvider} from "next-intl"; +import TekniskFeil from "../sider/feilsider/TekniskFeil.tsx"; +import {useEffect, useState} from "react"; + +export default function GlobalError({error, reset}: {error: Error & {digest?: string}; reset: () => void}) { + configureLogger({basePath: BASE_PATH, apiPath: `${BASE_PATH}/api`}); + + useEffect(() => { + if (process.env.NEXT_PUBLIC_DIGISOS_ENV !== "localhost") + logger.error(`En bruker har sett GlobalError, error: ${error} referrer: ${document.referrer}`); + }, [error]); + const langCookie = Cookie.get(DECORATOR_LANG_COOKIE); + const locale = langCookie && isSupportedLanguage(langCookie) ? langCookie : "nb"; + const [messages, setMessages] = useState(); + import(`../../messages/${locale}.json`).then((module) => module.default).then(setMessages); + + return ( + + + Søknad om økonomisk sosialhjelp + + +
+ {messages && ( + + + + )} +
+ + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 000000000..beccb5ede --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,44 @@ +import {fetchDecoratorReact} from "@navikt/nav-dekoratoren-moduler/ssr"; +import Script from "next/script"; +import {DECORATOR_SETTINGS} from "../decoratorSettings.tsx"; +import {isSupportedLanguage} from "../lib/i18n/common.ts"; +import {getMessages} from "next-intl/server"; +import {NextIntlClientProvider} from "next-intl"; +import "../index.css"; +import {cookies} from "next/headers"; +import {DECORATOR_LANG_COOKIE} from "../lib/constants.ts"; + +export const dynamic = "force-dynamic"; + +export default async function RootLayout({children}: {children: React.ReactNode}) { + const jar = await cookies(); + const cookie = jar.get(DECORATOR_LANG_COOKIE)?.value; + const locale = cookie && isSupportedLanguage(cookie) ? cookie : "nb"; + + const Decorator = await fetchDecoratorReact({ + ...DECORATOR_SETTINGS, + params: {...DECORATOR_SETTINGS.params, language: locale}, + }); + + const messages = await getMessages(); + // locale blir hentet via middleware.ts, + // og html lang leses (som document.documentElement.lang) av både analytics og klientside i18n + return ( + + + Søknad om økonomisk sosialhjelp + + + + +
+ + {children} + +
+ + + + + ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 000000000..925921895 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,13 @@ +"use client"; + +import IkkeFunnet from "../sider/feilsider/IkkeFunnet.tsx"; + +const NotFound = () => { + return ( +
+ +
+ ); +}; + +export default NotFound; diff --git a/src/faro.ts b/src/faro.ts index 00038f8a2..785ee1c58 100644 --- a/src/faro.ts +++ b/src/faro.ts @@ -11,9 +11,13 @@ import { import {TracingInstrumentation} from "@grafana/faro-web-tracing"; import digisosConfig from "./lib/config"; import {logger} from "@navikt/next-logger"; +let userHasBeenToldFaroIsDisabled = false; if (!digisosConfig.faro) { - logger.debug("faro is disabled!"); + if (!userHasBeenToldFaroIsDisabled) { + logger.debug("faro is disabled!"); + userHasBeenToldFaroIsDisabled = true; + } } else initializeFaro({ url: digisosConfig.faro.url, diff --git a/src/getPathPrefixIncludingLocale.ts b/src/getPathPrefixIncludingLocale.ts index 36f602151..d6214077b 100644 --- a/src/getPathPrefixIncludingLocale.ts +++ b/src/getPathPrefixIncludingLocale.ts @@ -8,7 +8,7 @@ import {BASE_PATH} from "./lib/constants.ts"; * Denne adapteren tilpasser BrowserRouter dette ved å forlenge basename med locale, * sånn at BrowserRouter kan ignorere locale-prefixet. */ -export const getPathPrefixIncludingLocale = (): [string, string] => { +export const getPathPrefixIncludingLocale = (): {prefix: string; path: string} => { const {pathname} = window.location; const pathMinusBasepath = pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname; const locale = getFirstSegmentOfPath(pathMinusBasepath); @@ -18,11 +18,11 @@ export const getPathPrefixIncludingLocale = (): [string, string] => { if (isSupportedLanguage(locale)) { const pathMinusLocaleToo = pathMinusBasepath.slice(locale.length + 1); - return [`${BASE_PATH}/${locale}`, pathMinusLocaleToo]; + return {prefix: `${BASE_PATH}/${locale}`, path: pathMinusLocaleToo}; } // Return the original BASE_PATH if no locale is found - return [BASE_PATH, pathMinusBasepath]; + return {prefix: BASE_PATH, path: pathMinusBasepath}; }; // Gets the first segment of a path, ignoring leading/trailing slashes diff --git a/src/lib/driftsmeldinger/Driftsmeldinger.tsx b/src/lib/driftsmeldinger/Driftsmeldinger.tsx index 62468fb9f..7ed44489f 100644 --- a/src/lib/driftsmeldinger/Driftsmeldinger.tsx +++ b/src/lib/driftsmeldinger/Driftsmeldinger.tsx @@ -1,9 +1,16 @@ +"use client"; import {Alert} from "@navikt/ds-react"; import Markdown from "markdown-to-jsx"; import {getDriftsmeldinger} from "./getDriftsmeldinger.ts"; +import {useEffect, useState} from "react"; +import {Driftsmelding} from "./types.ts"; -export const Driftsmeldinger = async () => - (await getDriftsmeldinger())?.map(({severity, text}) => ( +export const Driftsmeldinger = () => { + const [driftsmeldinger, setDriftsmeldinger] = useState(null); + useEffect(() => { + getDriftsmeldinger().then(setDriftsmeldinger); + }, []); + return driftsmeldinger?.map(({severity, text}) => ( {text} )); +}; diff --git a/src/lib/providers/DigisosContext.ts b/src/lib/providers/DigisosContext.ts index 1f40287ae..c91e85c92 100644 --- a/src/lib/providers/DigisosContext.ts +++ b/src/lib/providers/DigisosContext.ts @@ -1,6 +1,7 @@ import {createContext, Dispatch} from "react"; import {ValideringActionTypes, ValideringState} from "../validering.ts"; import {FeatureToggles200, SessionResponse} from "../../generated/model/index.ts"; +import {SupportedLanguage} from "../i18n/common.ts"; type TDigisosContext = { analytics: { @@ -13,6 +14,7 @@ type TDigisosContext = { }; featureToggles: FeatureToggles200; sessionInfo: SessionResponse; + locale: SupportedLanguage; }; export const DigisosContext = createContext(undefined); diff --git a/src/lib/providers/DigisosContextProvider.tsx b/src/lib/providers/DigisosContextProvider.tsx index b79693bde..83060ae87 100644 --- a/src/lib/providers/DigisosContextProvider.tsx +++ b/src/lib/providers/DigisosContextProvider.tsx @@ -6,8 +6,9 @@ import {getSessionInfo} from "../../generated/informasjon-ressurs/informasjon-re import {featureToggles as getFeatureToggles} from "../../generated/feature-toggle-ressurs/feature-toggle-ressurs.ts"; import {SessionResponse} from "../../generated/model/sessionResponse.ts"; import {FeatureToggles200} from "../../generated/model/featureToggles200.ts"; +import {SupportedLanguage} from "../i18n/common.ts"; -export const DigisosContextProvider = ({children}: {children: ReactNode}) => { +export const DigisosContextProvider = ({children, locale}: {children: ReactNode; locale: SupportedLanguage}) => { const [state, dispatch] = useReducer(valideringsReducer, initialValideringState); const [analyticsData, setAnalyticsDataState] = useState({}); @@ -41,6 +42,7 @@ export const DigisosContextProvider = ({children}: {children: ReactNode}) => { }, featureToggles: featureToggles!, sessionInfo: sessionInfo!, + locale, }} > {children} diff --git a/src/middleware.ts b/src/middleware.ts index 09a0e4198..41e7f1d19 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,7 @@ -import {configureLogger} from "@navikt/next-logger"; import createMiddleware from "next-intl/middleware"; import {routing} from "./i18n/routing.ts"; import {NextRequest} from "next/server"; -configureLogger({basePath: "/sosialhjelp/soknad", apiPath: "/sosialhjelp/soknad/api"}); - export default async function middleware(request: NextRequest) { return createMiddleware(routing)(request); } diff --git a/src/sider/feilsider/ErrorDump.tsx b/src/sider/feilsider/ErrorDump.tsx index f547eccf9..344807533 100644 --- a/src/sider/feilsider/ErrorDump.tsx +++ b/src/sider/feilsider/ErrorDump.tsx @@ -2,15 +2,18 @@ import {BodyShort, Heading} from "@navikt/ds-react"; import StackTracey from "stacktracey"; import * as React from "react"; -export const ErrorDump = ({error}: {error: Error}) => ( -
- - Feilmelding: - - {error.message} - - Stacktrace: - -
{new StackTracey(error).clean().asTable()}
-
-); +export const ErrorDump = ({error}: {error: Error}) => { + const stackTrace = new StackTracey(error).clean().asTable(); + return ( +
+ + Feilmelding: + + {error.message} + + Stacktrace: + +
{stackTrace}
+
+ ); +}; diff --git a/src/sider/feilsider/IkkeFunnet.tsx b/src/sider/feilsider/IkkeFunnet.tsx index 463f2f1dd..3b70fd87e 100644 --- a/src/sider/feilsider/IkkeFunnet.tsx +++ b/src/sider/feilsider/IkkeFunnet.tsx @@ -9,7 +9,7 @@ import {useTranslations} from "next-intl"; const IkkeFunnet = () => { const tN = useTranslations("IkkeFunnet"); return ( -
+
} diff --git a/src/sider/feilsider/TekniskFeil.tsx b/src/sider/feilsider/TekniskFeil.tsx index 98b2929f7..711abffad 100644 --- a/src/sider/feilsider/TekniskFeil.tsx +++ b/src/sider/feilsider/TekniskFeil.tsx @@ -8,7 +8,7 @@ import {useTranslations} from "next-intl"; import {ErrorPageColumnarLayout} from "./ErrorPageColumnarLayout.tsx"; import {ErrorDump} from "./ErrorDump.tsx"; -export const TekniskFeil = ({error}: {error: Error}) => { +export const TekniskFeil = ({error, reset}: {error: Error; reset: () => void}) => { const t = useTranslations("TekniskFeil"); useTitle(`Feilside - ${document.location.hostname}`); @@ -30,6 +30,12 @@ export const TekniskFeil = ({error}: {error: Error}) => { {t("anbefaling")} +
+ +
+