diff --git a/eslint.config.mjs b/eslint.config.mjs index c6f55a4..33e0485 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,7 +12,7 @@ const compat = new FlatCompat({ const eslintConfig = [ { - ignores: ["src/network/generated/**"], + ignores: ["src/network/generated/**", ".next/**"], }, ...compat.extends("next/core-web-vitals", "next/typescript"), ...pluginQuery.configs["flat/recommended"], diff --git a/package-lock.json b/package-lock.json index 979d481..75db390 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,7 @@ "react": "^19.0.0", "react-dom": "^19.2.3", "tailwind-variants": "^1.0.0", - "zod": "^4.3.5", - "zustand": "^5.0.11" + "zod": "^4.3.5" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1704,7 +1703,7 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2931,7 +2930,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -7819,35 +7818,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zustand": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", - "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index bfa7a8e..dd5ebc8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --report-unused-disable-directives --max-warnings=0", "prettier": "prettier --write .", "type-check": "tsc --noEmit", "generate:profiel": "openapi-typescript ./dependencies/profiel-service/openapi.yaml -o ./src/network/profiel/generated.ts", @@ -29,8 +29,7 @@ "react": "^19.0.0", "react-dom": "^19.2.3", "tailwind-variants": "^1.0.0", - "zod": "^4.3.5", - "zustand": "^5.0.11" + "zod": "^4.3.5" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/app/actions.tsx b/src/app/actions.tsx new file mode 100644 index 0000000..99edcad --- /dev/null +++ b/src/app/actions.tsx @@ -0,0 +1,44 @@ +"use server"; + +import { + defaultFlags, + FeatureFlags, + featureFlagsSchema, +} from "@/app/instellingen/_featureFlags"; +import { cookies } from "next/headers"; + +// We can't use the useCookie hook here, because it's meant for client-side usage. Instead, we directly interact with the cookies API provided by Next.js in server actions. +export async function setFeatureFlagsCookie(flags: FeatureFlags) { + // Validate with Zod + const result = featureFlagsSchema.safeParse(flags); + if (!result.success) { + throw new Error( + "Invalid feature flags: " + JSON.stringify(result.error.format()), + ); + } + + const cookiesStore = await cookies(); + cookiesStore.set("flags", JSON.stringify(flags), { + path: "/", + httpOnly: true, + }); +} + +export const getFlagsFromServerCookie = async () => { + const cookieStore = await cookies(); + const flagsCookie = cookieStore.get("flags")?.value; + + let flags = {} as FeatureFlags; + if (flagsCookie) { + try { + flags = JSON.parse(decodeURIComponent(flagsCookie)); + const result = featureFlagsSchema.safeParse(flags); + if (!result.success) { + flags = defaultFlags; + } + } catch { + flags = defaultFlags; + } + } + return flags; +}; diff --git a/src/app/contactgegevens/[type]/_contactEditBox.tsx b/src/app/contactgegevens/[type]/_contactEditBox.tsx index 6ffabed..c38d087 100644 --- a/src/app/contactgegevens/[type]/_contactEditBox.tsx +++ b/src/app/contactgegevens/[type]/_contactEditBox.tsx @@ -78,7 +78,7 @@ export const ContactEditBox = ({ // setStatus("available"); }, onError: (error: Error) => { - console.log(error); + console.error(error); }, }, ); diff --git a/src/app/instellingen/_featureFlags.ts b/src/app/instellingen/_featureFlags.ts new file mode 100644 index 0000000..d8eaf5a --- /dev/null +++ b/src/app/instellingen/_featureFlags.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +// Define the schema with explicit keys +export const featureFlagsSchema = z.object({ + feature_MijnZaken: z.boolean(), + feature_MijnTaken: z.boolean(), + feature_MijnProducten: z.boolean(), + feature_RegelRecht: z.boolean(), +}); + +export type FeatureFlags = z.infer; +export type FeatureFlagKey = keyof FeatureFlags; + +export const defaultFlags: FeatureFlags = { + feature_MijnZaken: false, + feature_MijnTaken: false, + feature_MijnProducten: false, + feature_RegelRecht: false, +}; diff --git a/src/app/instellingen/_toggleFeature.tsx b/src/app/instellingen/_toggleFeature.tsx new file mode 100644 index 0000000..034ca57 --- /dev/null +++ b/src/app/instellingen/_toggleFeature.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { + defaultFlags, + FeatureFlagKey, + FeatureFlags, +} from "@/app/instellingen/_featureFlags"; +import { setFeatureFlagsCookie } from "../actions"; + +export const ToggleFeature = ({ + flags, + featureLabel, + featureName, +}: { + flags: FeatureFlags; + featureLabel: string; + featureName: FeatureFlagKey; +}) => { + const isToggled = flags[featureName]; + + const handleToggle = async () => { + setFeatureFlagsCookie({ + ...defaultFlags, + ...flags, + [featureName]: !flags[featureName], // toggle + }); + }; + + return ( +
+ + +
{ + if (e.key === "Enter" || e.key === " ") handleToggle(); + }} + > +
+
+
+ ); +}; diff --git a/src/app/instellingen/page.tsx b/src/app/instellingen/page.tsx index 2104a11..486e415 100644 --- a/src/app/instellingen/page.tsx +++ b/src/app/instellingen/page.tsx @@ -1,12 +1,10 @@ -"use client"; import Card from "@/components/card"; -import { - useFeatureFlagsStore, - useHydratedFeatureFlags, - type FeatureFlagKey, -} from "@/stores/featureFlags"; +import { getFlagsFromServerCookie } from "../actions"; +import { ToggleFeature } from "./_toggleFeature"; + +const Page = async () => { + const flags = await getFlagsFromServerCookie(); -const Page = () => { return ( <>

Instellingen

@@ -21,18 +19,22 @@ const Page = () => {
@@ -42,48 +44,4 @@ const Page = () => { ); }; -const ToggleFeature = ({ - featureLabel, - featureName, -}: { - featureLabel: string; - featureName: FeatureFlagKey; -}) => { - const isHydrated = useHydratedFeatureFlags(); - const isToggled = useFeatureFlagsStore((s) => s.flags[featureName]); - console.log(isToggled); - const toggleFlag = useFeatureFlagsStore((s) => s.toggleFlag); - - const handleToggle = () => toggleFlag(featureName); - - if (!isHydrated) return null; - - return ( -
- - -
{ - if (e.key === "Enter" || e.key === " ") handleToggle(); - }} - > -
-
-
- ); -}; - export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 395832a..43bc955 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import { getKvkFromCookie, getKvkOptionsFromCookie } from "@/utils/kvknummer"; import { Footer } from "@/layouts/footer"; import Breadcrumb from "@/layouts/breadcrumb"; import PublicRootLayout from "./publiclayout"; +import { getFlagsFromServerCookie } from "./actions"; export const metadata: Metadata = { title: "Mijn overheid zakelijk", @@ -19,12 +20,13 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const flags = await getFlagsFromServerCookie(); const session = await auth(); const kvk = await getKvkFromCookie(); const kvkOpties = await getKvkOptionsFromCookie(); return ( - + {!session ? ( @@ -35,7 +37,7 @@ export default async function RootLayout({
- +
diff --git a/src/layouts/navigation/index.tsx b/src/layouts/navigation/index.tsx index 7cb45ea..61251b1 100644 --- a/src/layouts/navigation/index.tsx +++ b/src/layouts/navigation/index.tsx @@ -2,91 +2,12 @@ import { ReactNode } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { - useFeatureFlagsStore, - useHydratedFeatureFlags, -} from "@/stores/featureFlags"; +import { defaultFlags, FeatureFlags } from "@/app/instellingen/_featureFlags"; -const NavigationSkeleton = () => { - return ( -
-
    -
  • -
    -
    -
    -
    -
  • -
- -
    -
  • -
    -
    -
    -
    -
  • -
  • -
    -
    -
    -
    -
  • -
- -
    -
  • -
    -
    -
    -
    -
  • -
  • -
    -
    -
    -
    -
  • -
  • -
    -
    -
    -
    -
  • -
  • -
    -
    -
    -
    -
  • -
  • -
    -
    -
    -
    -
  • -
- -
    -
  • -
    -
    -
    -
    -
  • -
-
- ); -}; - -const Navigation = () => { +const Navigation = ({ flags = defaultFlags }: { flags: FeatureFlags }) => { const pathname = usePathname(); - const isHydrated = useHydratedFeatureFlags(); - const currentFlags = useFeatureFlagsStore((s) => s.flags); - - const hasAnyTrueFlag = Object.values(currentFlags).some((flag) => flag); - if (!isHydrated) return ; + const hasAnyTrueFlag = Object.values(flags).some((flag) => flag === true); return (
@@ -130,7 +51,7 @@ const Navigation = () => { {hasAnyTrueFlag && (
    - {currentFlags.feature_MijnZaken && ( + {flags.feature_MijnZaken && ( { /> )} - {currentFlags.feature_MijnTaken && ( + {flags.feature_MijnTaken && ( { /> )} - {currentFlags.feature_MijnProducten && ( + {flags.feature_MijnProducten && ( { /> )} - {currentFlags.feature_RegelRecht && ( + {flags.feature_RegelRecht && ( ; - setFlag: (key: FeatureFlagKey, value: boolean) => void; - toggleFlag: (key: FeatureFlagKey) => void; - isEnabled: (key: FeatureFlagKey) => boolean; -}; - -export const useFeatureFlagsStore = create()( - persist( - (set, get) => ({ - flags: { - feature_MijnZaken: false, - feature_MijnTaken: false, - feature_MijnProducten: false, - feature_RegelRecht: false, - }, - - setFlag: (key, value) => - set((state) => ({ - flags: { ...state.flags, [key]: value }, - })), - - toggleFlag: (key) => - set((state) => ({ - flags: { ...state.flags, [key]: !state.flags[key] }, - })), - - isEnabled: (key) => get().flags[key], - }), - { - name: "feature-flags", // localStorage key - partialize: (state) => ({ flags: state.flags }), // only persist flags - skipHydration: true, - }, - ), -); - -export const useHydratedFeatureFlags = () => { - const [hydrated, setHydrated] = useState(false); - - useEffect(() => { - useFeatureFlagsStore.persist.rehydrate(); - setHydrated(true); - }, []); - - return hydrated; -};