diff --git a/.storybook/main.ts b/.storybook/main.ts index bf80817..333d4fa 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,6 +22,10 @@ const config: StorybookConfig = { "../public" ], + features: { + experimentalRSC: true, + }, + docs: { autodocs: true }, @@ -30,4 +34,4 @@ const config: StorybookConfig = { reactDocgen: "react-docgen-typescript" } }; -export default config; \ No newline at end of file +export default config; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..3e4855d --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,13 @@ + + + + diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 79284ac..1e1717f 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,12 @@ import type { Preview } from "@storybook/react"; -import "../src/app/globals.css"; +import "@/app/[lang]/globals.css"; +import { + getRouter, + usePathname, +} from "@storybook/nextjs/navigation.mock"; +import mockRouter from "next-router-mock"; +import { i18n } from "../src/i18n/i18n-config"; + const preview: Preview = { parameters: { controls: { @@ -8,6 +15,33 @@ const preview: Preview = { date: /Date$/i, }, }, + nextjs: { + appDirectory: true, + }, + }, + beforeEach: () => { + getRouter().push.mockImplementation( + (...args: Parameters) => mockRouter.push(...args) + ); + getRouter().replace.mockImplementation( + (...args: Parameters) => + mockRouter.replace(...args) + ); + + usePathname.mockImplementation(() => { + const pathname = mockRouter.pathname || `/${i18n.defaultLocale}`; + const segments = pathname.split("/"); + const isLocaleMissing = i18n.locales.every( + (locale) => + !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` + ); + + if (isLocaleMissing) { + segments[1] = i18n.defaultLocale; + } + + return segments.join("/"); + }); }, }; diff --git a/package-lock.json b/package-lock.json index 1b3f2cd..f05be0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "e-learning", "version": "0.1.0", "dependencies": { + "@formatjs/intl-localematcher": "^0.6.1", "@phosphor-icons/react": "^2.1.7", "clsx": "^2.1.1", + "negotiator": "^1.0.0", "next": "15.2.4", + "next-router-mock": "^0.9.13", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-merge": "^3.2.0" @@ -30,6 +33,7 @@ "@tailwindcss/postcss": "^4", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", + "@types/negotiator": "^0.6.3", "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", @@ -2691,6 +2695,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -5339,6 +5352,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.17.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", @@ -11839,6 +11859,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -11900,6 +11929,16 @@ } } }, + "node_modules/next-router-mock": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.13.tgz", + "integrity": "sha512-906n2RRaE6Y28PfYJbaz5XZeJ6Tw8Xz1S6E31GGwZ0sXB6/XjldD1/2azn1ZmBmRk5PQRkzjg+n+RHZe5xQzWA==", + "license": "MIT", + "peerDependencies": { + "next": ">=10.0.0", + "react": ">=17.0.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 623fc62..6504948 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ "test:e2e": "npx playwright test" }, "dependencies": { + "@formatjs/intl-localematcher": "^0.6.1", "@phosphor-icons/react": "^2.1.7", + "clsx": "^2.1.1", + "negotiator": "^1.0.0", "next": "15.2.4", + "next-router-mock": "^0.9.13", "react": "^19.0.0", "react-dom": "^19.0.0", - "clsx": "^2.1.1", "tailwind-merge": "^3.2.0" }, "devDependencies": { @@ -37,6 +40,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/negotiator": "^0.6.3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/favicon.ico b/src/app/[lang]/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to src/app/[lang]/favicon.ico diff --git a/src/app/globals.css b/src/app/[lang]/globals.css similarity index 100% rename from src/app/globals.css rename to src/app/[lang]/globals.css diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx new file mode 100644 index 0000000..5c6dbab --- /dev/null +++ b/src/app/[lang]/layout.tsx @@ -0,0 +1,31 @@ +import { i18n, type Locale } from "@/i18n/i18n-config"; + +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); + +export const metadata: Metadata = { + title: "E-Learning", +}; + +export async function generateStaticParams() { + return i18n.locales.map((locale) => ({ lang: locale })); +} + +export default async function RootLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ lang: Locale }>; +}>) { + const { lang } = await params; + + return ( + + {children} + + ); +} diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx new file mode 100644 index 0000000..498a34f --- /dev/null +++ b/src/app/[lang]/page.tsx @@ -0,0 +1,25 @@ +import { getDictionary } from "@/i18n/get-dictionary"; +import { Locale } from "@/i18n/i18n-config"; +import { Header } from "@/components/Header/Header"; +import { Button } from "@/components/Button/Default/Button"; + +export default async function Home({ params }: { params: { lang: Locale } }) { + const { lang } = await params; + + const dictionary = await getDictionary(lang); + + return ( +
+
+ +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index b2d238e..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); - -export const metadata: Metadata = { - title: "E-Learning", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx deleted file mode 100644 index 37bfb2c..0000000 --- a/src/app/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Button } from "@/components/Button/Default/Button"; -import { ArrowLeft } from "@phosphor-icons/react"; - -export default function Home() { - return ( -
-
- ); -} diff --git a/src/components/Header/Header.stories.tsx b/src/components/Header/Header.stories.tsx new file mode 100644 index 0000000..127bb20 --- /dev/null +++ b/src/components/Header/Header.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Header } from "./Header"; +import { getDictionary } from "@/i18n/get-dictionary"; +import { i18n } from "@/i18n/i18n-config"; // Importa as configurações de idiomas + +const meta: Meta = { + title: "Components/Header", + component: Header, + argTypes: { + lang: { + control: "select", + options: i18n.locales, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + lang: i18n.defaultLocale, + dictionary: (await getDictionary(i18n.defaultLocale)).header, + }, +}; diff --git a/src/components/Header/Header.test.tsx b/src/components/Header/Header.test.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..4bed4ce --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import Link from "next/link"; +import { Typography } from "@/components/Typography/Typography"; +import { cn } from "@/lib/utils"; +import { type getDictionary } from "@/i18n/get-dictionary"; +import LocaleSwitcher from "@/components/Header/LocaleSwitcher/LocaleSwitcher"; + +export const Header = ({ + dictionary, + lang, +}: { + dictionary: Awaited>["header"]; + lang: string; +}) => { + const pathname = usePathname(); + + const navItems = [ + { redirect: `/${lang}`, name: dictionary.nav.home }, + { redirect: `/${lang}/courses`, name: dictionary.nav.courses }, + { redirect: `/${lang}/about`, name: dictionary.nav.about }, + { redirect: `/${lang}/contact`, name: dictionary.nav.contact }, + { redirect: `/${lang}/instructor`, name: dictionary.nav.instructor }, + ]; + + const renderNavItems = () => { + return navItems.map((item) => { + const isActive = pathname.startsWith(item.redirect); + + return ( + + {item.name} + + ); + }); + }; + + return ( +
+ +
+ {/* TODO implementar componente de select */} + + +
+
+ ); +}; diff --git a/src/components/Header/LocaleSwitcher/LocaleSwitcher.tsx b/src/components/Header/LocaleSwitcher/LocaleSwitcher.tsx new file mode 100644 index 0000000..2b12f20 --- /dev/null +++ b/src/components/Header/LocaleSwitcher/LocaleSwitcher.tsx @@ -0,0 +1,42 @@ +import { Select } from "@/components/Select/Select"; +import { i18n, type Locale } from "@/i18n/i18n-config"; +import { usePathname, useRouter } from "next/navigation"; +import translations from "@/i18n/locales/index"; + +export default function LocaleSwitcher() { + const pathname = usePathname(); + const router = useRouter(); + + const redirectedPathname = (locale: Locale) => { + if (!pathname) return "/"; + const segments = pathname.split("/"); + segments[1] = locale; + return segments.join("/"); + }; + + const handleLocaleChange = (locale: string) => { + const newPath = redirectedPathname(locale as Locale); + router.push(newPath); + }; + + const activeLocale = pathname?.split("/")[1] as Locale; + + const activeLabel = translations[activeLocale]?.header.language; + + const localeOptions = i18n.locales.map((locale) => ({ + value: locale, + label: translations[locale]?.header.language || locale, + })); + + console.log(activeLabel); + + return ( +