From 67074b14ba428443c7394566444a420942be099a Mon Sep 17 00:00:00 2001 From: nike-nike21 Date: Mon, 1 Jun 2026 22:54:45 +0100 Subject: [PATCH 1/2] feat(frontend): add i18n, form validation, storybook docs, and performance monitoring --- eslint.config.js | 27 +- frontend/.storybook/main.ts | 20 + frontend/.storybook/preview.tsx | 21 + frontend/app/components/Navbar.tsx | 79 +- frontend/app/components/Newsletter.tsx | 78 +- .../app/dashboard/settings/SettingsClient.tsx | 130 +- frontend/app/layout.tsx | 44 +- frontend/app/lib/locales/en.json | 95 + frontend/app/lib/locales/es.json | 95 + .../create-goal/components/CreateGoalForm.tsx | 326 +- .../create-goal/components/GoalForm.tsx | 236 +- frontend/app/support/page.tsx | 158 + frontend/debug-storybook.log | 27 + frontend/middleware.ts | 35 + frontend/next.config.ts | 5 +- frontend/package-lock.json | 749 ++- frontend/package.json | 24 +- frontend/pnpm-lock.yaml | 4269 ++++++++++++++++- frontend/stories/Button.stories.ts | 54 + frontend/stories/Button.tsx | 39 + frontend/stories/Configure.mdx | 446 ++ frontend/stories/Header.stories.ts | 34 + frontend/stories/Header.tsx | 54 + frontend/stories/Page.stories.ts | 33 + frontend/stories/Page.tsx | 73 + frontend/stories/assets/accessibility.png | Bin 0 -> 42336 bytes frontend/stories/assets/accessibility.svg | 1 + frontend/stories/assets/addon-library.png | Bin 0 -> 467366 bytes frontend/stories/assets/assets.png | Bin 0 -> 3899 bytes frontend/stories/assets/avif-test-image.avif | Bin 0 -> 829 bytes frontend/stories/assets/context.png | Bin 0 -> 6119 bytes frontend/stories/assets/discord.svg | 1 + frontend/stories/assets/docs.png | Bin 0 -> 27875 bytes frontend/stories/assets/figma-plugin.png | Bin 0 -> 44246 bytes frontend/stories/assets/github.svg | 1 + frontend/stories/assets/share.png | Bin 0 -> 40767 bytes frontend/stories/assets/styling.png | Bin 0 -> 7237 bytes frontend/stories/assets/testing.png | Bin 0 -> 49313 bytes frontend/stories/assets/theming.png | Bin 0 -> 44374 bytes frontend/stories/assets/tutorials.svg | 1 + frontend/stories/assets/youtube.svg | 1 + frontend/stories/button.css | 30 + frontend/stories/header.css | 32 + frontend/stories/page.css | 68 + frontend/vitest.config.ts | 36 + frontend/vitest.shims.d.ts | 1 + 46 files changed, 6701 insertions(+), 622 deletions(-) create mode 100644 frontend/.storybook/main.ts create mode 100644 frontend/.storybook/preview.tsx create mode 100644 frontend/app/lib/locales/en.json create mode 100644 frontend/app/lib/locales/es.json create mode 100644 frontend/app/support/page.tsx create mode 100644 frontend/debug-storybook.log create mode 100644 frontend/middleware.ts create mode 100644 frontend/stories/Button.stories.ts create mode 100644 frontend/stories/Button.tsx create mode 100644 frontend/stories/Configure.mdx create mode 100644 frontend/stories/Header.stories.ts create mode 100644 frontend/stories/Header.tsx create mode 100644 frontend/stories/Page.stories.ts create mode 100644 frontend/stories/Page.tsx create mode 100644 frontend/stories/assets/accessibility.png create mode 100644 frontend/stories/assets/accessibility.svg create mode 100644 frontend/stories/assets/addon-library.png create mode 100644 frontend/stories/assets/assets.png create mode 100644 frontend/stories/assets/avif-test-image.avif create mode 100644 frontend/stories/assets/context.png create mode 100644 frontend/stories/assets/discord.svg create mode 100644 frontend/stories/assets/docs.png create mode 100644 frontend/stories/assets/figma-plugin.png create mode 100644 frontend/stories/assets/github.svg create mode 100644 frontend/stories/assets/share.png create mode 100644 frontend/stories/assets/styling.png create mode 100644 frontend/stories/assets/testing.png create mode 100644 frontend/stories/assets/theming.png create mode 100644 frontend/stories/assets/tutorials.svg create mode 100644 frontend/stories/assets/youtube.svg create mode 100644 frontend/stories/button.css create mode 100644 frontend/stories/header.css create mode 100644 frontend/stories/page.css create mode 100644 frontend/vitest.config.ts create mode 100644 frontend/vitest.shims.d.ts diff --git a/eslint.config.js b/eslint.config.js index 02d37fcb8..8a2055cc1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,17 +1,18 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from "eslint-plugin-storybook"; + import tsParser from "@typescript-eslint/parser"; -export default [ - { - files: ["**/*.{js,jsx,ts,tsx}"], - ignores: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"], - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { jsx: true }, - }, +export default [{ + files: ["**/*.{js,jsx,ts,tsx}"], + ignores: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { jsx: true }, }, - rules: {}, }, -]; + rules: {}, +}, ...storybook.configs["flat/recommended"]]; diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 000000000..046788225 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,20 @@ +import type { StorybookConfig } from '@storybook/nextjs-vite'; + +const config: StorybookConfig = { + "stories": [ + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + "addons": [ + "@chromatic-com/storybook", + "@storybook/addon-vitest", + "@storybook/addon-a11y", + "@storybook/addon-docs", + "@storybook/addon-mcp" + ], + "framework": "@storybook/nextjs-vite", + "staticDirs": [ + "..\\public" + ] +}; +export default config; \ No newline at end of file diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..eb97b831d --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,21 @@ +import type { Preview } from '@storybook/nextjs-vite' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo' + } + }, +}; + +export default preview; \ No newline at end of file diff --git a/frontend/app/components/Navbar.tsx b/frontend/app/components/Navbar.tsx index 7813baa79..5d7d9f6c4 100644 --- a/frontend/app/components/Navbar.tsx +++ b/frontend/app/components/Navbar.tsx @@ -1,9 +1,10 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { Wallet } from "lucide-react"; +import { useTranslations } from "next-intl"; interface NavLink { label: string; @@ -12,18 +13,23 @@ interface NavLink { const Navbar: React.FC = () => { const pathname = usePathname(); + const router = useRouter(); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const t = useTranslations("Navbar"); const navLinks: NavLink[] = [ - { label: "Features", href: "/features" }, - { label: "Savings", href: "/savings" }, - { label: "Dashboard", href: "/dashboard" }, - { label: "Community", href: "/community" }, - { label: "Docs", href: "/docs" }, + { label: t("features"), href: "/features" }, + { label: t("savings"), href: "/savings" }, + { label: t("dashboard"), href: "/dashboard" }, + { label: t("community"), href: "/community" }, + { label: t("docs"), href: "/docs" }, ]; const isActiveLink = (href: string): boolean => { - return pathname === href || pathname?.startsWith(href + "/"); + // Remove locale prefix for comparison + const cleanPathname = pathname.replace(/^\/[a-z]{2}/, ""); + const cleanHref = href.replace(/^\/[a-z]{2}/, ""); + return cleanPathname === cleanHref || cleanPathname?.startsWith(cleanHref + "/"); }; const navLinkBase = @@ -36,6 +42,13 @@ const Navbar: React.FC = () => { const mobileLinkActive = "text-cyan-500 bg-slate-800 border-l-cyan-500"; + const handleLanguageChange = (locale: string) => { + // Get current pathname without locale + const currentPath = pathname.replace(/^\/[a-z]{2}/, "") || "/"; + // Navigate to the same path with new locale + router.push(`/(${locale})${currentPath}`); + }; + return (