create-next-pro-cli 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -292
- package/create-next-pro-completion.sh +8 -38
- package/create-next-pro-completion.zsh +13 -0
- package/dist/bin.bun.js +1127 -628
- package/dist/bin.node.js +1170 -617
- package/dist/bin.node.js.map +1 -1
- package/dist/create-next-pro +35 -6
- package/package.json +49 -27
- package/templates/Projects/default/.env.example +17 -0
- package/templates/Projects/default/.github/workflows/quality.yml +24 -0
- package/templates/Projects/default/.gitignore.template +47 -0
- package/templates/Projects/default/.prettierignore +3 -0
- package/templates/Projects/default/README.md +66 -21
- package/templates/Projects/default/bun.lock +1152 -0
- package/templates/Projects/default/eslint.config.mjs +27 -11
- package/templates/Projects/default/messages/en/_global_ui.json +23 -10
- package/templates/Projects/default/messages/en.ts +17 -2
- package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
- package/templates/Projects/default/messages/fr.ts +17 -2
- package/templates/Projects/default/next.config.ts +43 -3
- package/templates/Projects/default/package.json +42 -24
- package/templates/Projects/default/playwright.config.ts +26 -0
- package/templates/Projects/default/pnpm-workspace.yaml +5 -0
- package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
- package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
- package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
- package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
- package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
- package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
- package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
- package/templates/Projects/default/src/app/not-found.tsx +1 -1
- package/templates/Projects/default/src/app/sitemap.ts +2 -2
- package/templates/Projects/default/src/app/styles/globals.css +166 -113
- package/templates/Projects/default/src/auth.ts +20 -0
- package/templates/Projects/default/src/config.ts +3 -3
- package/templates/Projects/default/src/env.ts +65 -0
- package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
- package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
- package/templates/Projects/default/src/lib/security/csp.ts +16 -0
- package/templates/Projects/default/src/lib/utils.ts +2 -1
- package/templates/Projects/default/src/proxy.ts +13 -0
- package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
- package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
- package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
- package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
- package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
- package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
- package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
- package/templates/Projects/default/tailwind.config.ts +81 -1
- package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
- package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
- package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
- package/templates/Projects/default/tests/unit/env.test.ts +76 -0
- package/templates/Projects/default/tsconfig.json +6 -6
- package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
- package/templates/Projects/default/middleware.ts +0 -11
- package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
- package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
- package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
- package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
- package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
- package/templates/Projects/default/src/app/layout.tsx +0 -11
- package/templates/Projects/default/src/app/page.tsx +0 -6
- package/templates/Projects/default/src/auth.config.ts +0 -0
- package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
- package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
- package/templates/Projects/default/src/lib/sample/example.ts +0 -3
- package/templates/Projects/default/src/lib/sample/index.ts +0 -3
- package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
- package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
- package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
- package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
- /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
- /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
|
@@ -1,51 +1,33 @@
|
|
|
1
|
-
"use client";
|
|
2
1
|
import Image from "next/image";
|
|
3
2
|
import UserNav from "@/ui/_global/UserNav";
|
|
4
3
|
import LocaleSwitcher from "@/ui/_global/LocaleSwitcher";
|
|
5
4
|
import PublicNav from "@/ui/_global/PublicNav";
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
7
5
|
import { Link } from "@/lib/i18n/navigation";
|
|
8
|
-
import { isConnected } from "@/lib/auth/isConnected";
|
|
9
6
|
|
|
10
7
|
type GlobalHeaderProps = {
|
|
11
|
-
|
|
8
|
+
hasSessionInitial: boolean;
|
|
12
9
|
};
|
|
13
10
|
|
|
14
|
-
export default function GlobalHeader({
|
|
15
|
-
isConnectedInitial,
|
|
16
|
-
}: GlobalHeaderProps) {
|
|
17
|
-
const [isConnectedState, setIsConnectedState] = useState(isConnectedInitial);
|
|
18
|
-
|
|
19
|
-
// Vérification dynamique côté client (exemple via /api/me)
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
async function checkConnection() {
|
|
22
|
-
setIsConnectedState(await isConnected());
|
|
23
|
-
}
|
|
24
|
-
checkConnection();
|
|
25
|
-
// Optionnel : timer pour rafraîchir périodiquement
|
|
26
|
-
// const interval = setInterval(checkConnection, 60000);
|
|
27
|
-
// return () => clearInterval(interval);
|
|
28
|
-
}, []);
|
|
29
|
-
|
|
11
|
+
export default function GlobalHeader({ hasSessionInitial }: GlobalHeaderProps) {
|
|
30
12
|
return (
|
|
31
|
-
<header className="fixed top-0 w-full
|
|
32
|
-
<div className="mx-auto
|
|
33
|
-
<Link href="/" className="">
|
|
34
|
-
<div className="flex items-center
|
|
13
|
+
<header className="fixed top-0 z-50 w-full border-b bg-background/80 glass-effect">
|
|
14
|
+
<div className="mx-auto grid h-16 max-w-6xl grid-cols-[auto_1fr_auto] items-center gap-2 px-3 sm:px-4 md:px-6">
|
|
15
|
+
<Link href="/" aria-label="Create Next Pro home" className="min-w-0">
|
|
16
|
+
<div className="flex h-16 min-w-0 items-center">
|
|
35
17
|
<Image
|
|
36
|
-
src="/
|
|
18
|
+
src="/logo.svg"
|
|
37
19
|
alt="Logo"
|
|
38
20
|
width={63}
|
|
39
21
|
height={63}
|
|
40
|
-
className="mr-2"
|
|
22
|
+
className="mr-2 h-11 w-11 sm:h-[52px] sm:w-[52px] md:h-[63px] md:w-[63px]"
|
|
41
23
|
/>
|
|
42
|
-
<h1 className="text-
|
|
24
|
+
<h1 className="hidden whitespace-nowrap text-base font-semibold tracking-tight select-none sm:block md:text-lg">
|
|
43
25
|
Create Next Pro
|
|
44
26
|
</h1>
|
|
45
27
|
</div>
|
|
46
28
|
</Link>
|
|
47
29
|
|
|
48
|
-
{
|
|
30
|
+
{hasSessionInitial ? <UserNav /> : <PublicNav />}
|
|
49
31
|
<div className="flex items-center h-16">
|
|
50
32
|
<LocaleSwitcher />
|
|
51
33
|
</div>
|
|
@@ -6,7 +6,7 @@ export default function GlobalMain({
|
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
}) {
|
|
8
8
|
return (
|
|
9
|
-
<main className="mt-0 min-h-screen bg-neutral-50 font-sans
|
|
9
|
+
<main className="mt-0 flex min-h-screen flex-col bg-neutral-50 text-foreground font-sans hero-pattern">
|
|
10
10
|
<div className="flex-1 flex flex-col justify-center items-stretch mx-auto w-full px-6 self-stretch">
|
|
11
11
|
{children}
|
|
12
12
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
3
|
+
import { usePathname, useRouter } from "@/lib/i18n/navigation";
|
|
4
4
|
|
|
5
5
|
const locales = [
|
|
6
6
|
{ code: "fr", label: "Français" },
|
|
@@ -10,23 +10,22 @@ const locales = [
|
|
|
10
10
|
export default function LocaleSwitcher() {
|
|
11
11
|
const router = useRouter();
|
|
12
12
|
const pathname = usePathname();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const currentLocale = useLocale();
|
|
14
|
+
const t = useTranslations("_global_ui");
|
|
15
15
|
|
|
16
16
|
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
17
17
|
const newLocale = e.target.value;
|
|
18
18
|
if (newLocale === currentLocale) return;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
router.push(newPath);
|
|
19
|
+
|
|
20
|
+
router.replace(pathname, { locale: newLocale });
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
return (
|
|
25
24
|
<select
|
|
26
|
-
className="
|
|
25
|
+
className="w-[7.5rem] rounded px-2 py-1 text-sm ring dark:bg-stone-800 dark:text-gray-200 light:bg-stone-200 light:text-stone-800 sm:w-auto"
|
|
27
26
|
value={currentLocale}
|
|
28
27
|
onChange={handleChange}
|
|
29
|
-
aria-label="
|
|
28
|
+
aria-label={t("select_language")}
|
|
30
29
|
>
|
|
31
30
|
{locales.map((l) => (
|
|
32
31
|
<option key={l.code} value={l.code}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client";
|
|
1
|
+
"use client";
|
|
2
2
|
import { Link } from "@/lib/i18n/navigation";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
4
|
import { useEffect, useState, useRef } from "react";
|
|
@@ -8,14 +8,17 @@ import { Menu, X } from "lucide-react";
|
|
|
8
8
|
|
|
9
9
|
const navLinks = [
|
|
10
10
|
{ href: "/", labelKey: "public_nav.links.home" },
|
|
11
|
-
{ href: "/
|
|
12
|
-
{ href: "/
|
|
11
|
+
{ href: "/login", labelKey: "public_nav.links.login" },
|
|
12
|
+
{ href: "/register", labelKey: "public_nav.links.register" },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const PublicNav = () => {
|
|
16
16
|
const t = useTranslations("_global_ui");
|
|
17
17
|
const [open, setOpen] = useState(false);
|
|
18
18
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
20
|
+
const firstLinkRef = useRef<HTMLAnchorElement>(null);
|
|
21
|
+
|
|
19
22
|
useEffect(() => {
|
|
20
23
|
const handleClickOutside = (event: MouseEvent) => {
|
|
21
24
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
@@ -25,6 +28,7 @@ const PublicNav = () => {
|
|
|
25
28
|
|
|
26
29
|
if (open) {
|
|
27
30
|
document.addEventListener("mousedown", handleClickOutside);
|
|
31
|
+
firstLinkRef.current?.focus();
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
return () => {
|
|
@@ -32,32 +36,47 @@ const PublicNav = () => {
|
|
|
32
36
|
};
|
|
33
37
|
}, [open]);
|
|
34
38
|
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
41
|
+
if (event.key === "Escape" && open) {
|
|
42
|
+
setOpen(false);
|
|
43
|
+
menuButtonRef.current?.focus();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener("keydown", handleEscape);
|
|
47
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
48
|
+
}, [open]);
|
|
49
|
+
|
|
35
50
|
return (
|
|
36
|
-
<nav aria-label="Public navigation">
|
|
37
|
-
<div className="flex
|
|
38
|
-
<ul className="
|
|
51
|
+
<nav aria-label="Public navigation" className="justify-self-end">
|
|
52
|
+
<div className="flex h-16 items-center gap-1 sm:gap-2">
|
|
53
|
+
<ul className="hidden items-center gap-2 md:flex lg:gap-4">
|
|
39
54
|
{navLinks.map((link) => (
|
|
40
55
|
<li key={link.href}>
|
|
41
56
|
<Link
|
|
42
57
|
href={link.href}
|
|
43
|
-
className="inline-block rounded px-3 py-1.5 text-sm font-medium dark:text-
|
|
58
|
+
className="inline-block rounded px-3 py-1.5 text-sm font-medium dark:text-stone-100 light:text-stone-900 dark:hover:bg-white/10 light:hover:bg-black/10 transition-colors focus:outline-none focus-visible:ring-2"
|
|
44
59
|
>
|
|
45
60
|
{t(link.labelKey)}
|
|
46
61
|
</Link>
|
|
47
62
|
</li>
|
|
48
63
|
))}
|
|
49
64
|
</ul>
|
|
50
|
-
<div className="flex items-center
|
|
65
|
+
<div className="flex items-center">
|
|
51
66
|
<ThemeToggle />
|
|
52
67
|
</div>
|
|
53
|
-
<div className="
|
|
68
|
+
<div className="flex items-center md:hidden">
|
|
54
69
|
<Button
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
ref={menuButtonRef}
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="icon"
|
|
57
73
|
className="focus:outline-none"
|
|
74
|
+
onClick={() => setOpen((o) => !o)}
|
|
58
75
|
aria-label="Menu"
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
aria-controls="public-mobile-menu"
|
|
59
78
|
>
|
|
60
|
-
{open ? <X className="h-
|
|
79
|
+
{open ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
|
61
80
|
</Button>
|
|
62
81
|
</div>
|
|
63
82
|
</div>
|
|
@@ -66,19 +85,34 @@ const PublicNav = () => {
|
|
|
66
85
|
<div className="fixed inset-0 z-40 md:hidden">
|
|
67
86
|
<div className="absolute inset-0 bg-black/40" aria-hidden="true" />
|
|
68
87
|
<div
|
|
88
|
+
id="public-mobile-menu"
|
|
69
89
|
ref={menuRef}
|
|
70
|
-
|
|
90
|
+
role="dialog"
|
|
91
|
+
aria-modal="true"
|
|
92
|
+
aria-label={t("public_nav.menu")}
|
|
93
|
+
className="mobil-menu absolute top-20 left-1/2 z-50 flex w-11/12 max-w-xs -translate-x-1/2 flex-col gap-2 rounded-xl p-4 shadow-lg animate-fade-in"
|
|
71
94
|
>
|
|
72
|
-
<Link href="/
|
|
95
|
+
<Link ref={firstLinkRef} href="/" onClick={() => setOpen(false)}>
|
|
73
96
|
<Button
|
|
74
97
|
variant="ghost"
|
|
75
|
-
className="w-full
|
|
98
|
+
className="w-full justify-center cursor-pointer glass-effect"
|
|
99
|
+
>
|
|
100
|
+
{t("public_nav.links.home")}
|
|
101
|
+
</Button>
|
|
102
|
+
</Link>
|
|
103
|
+
<Link href="/login" onClick={() => setOpen(false)}>
|
|
104
|
+
<Button
|
|
105
|
+
variant="ghost"
|
|
106
|
+
className="w-full justify-center cursor-pointer glass-effect"
|
|
76
107
|
>
|
|
77
108
|
{t("public_nav.links.login")}
|
|
78
109
|
</Button>
|
|
79
110
|
</Link>
|
|
80
|
-
<Link href="/
|
|
81
|
-
<Button
|
|
111
|
+
<Link href="/register" onClick={() => setOpen(false)}>
|
|
112
|
+
<Button
|
|
113
|
+
className="w-full justify-center cursor-pointer"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
>
|
|
82
116
|
{t("public_nav.links.register")}
|
|
83
117
|
</Button>
|
|
84
118
|
</Link>
|
|
@@ -1,53 +1,59 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
3
|
import { Moon, Sun } from "lucide-react";
|
|
4
4
|
import { Button } from "./Button";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
html.classList.remove("light");
|
|
24
|
-
setIsDark(true);
|
|
25
|
-
localStorage.setItem("theme", "dark");
|
|
26
|
-
}
|
|
7
|
+
const themeChangeEvent = "template-theme-change";
|
|
8
|
+
|
|
9
|
+
function getThemeSnapshot() {
|
|
10
|
+
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getServerThemeSnapshot() {
|
|
14
|
+
return "light";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function subscribeToThemeChange(onStoreChange: () => void) {
|
|
18
|
+
window.addEventListener(themeChangeEvent, onStoreChange);
|
|
19
|
+
window.addEventListener("storage", onStoreChange);
|
|
20
|
+
|
|
21
|
+
return () => {
|
|
22
|
+
window.removeEventListener(themeChangeEvent, onStoreChange);
|
|
23
|
+
window.removeEventListener("storage", onStoreChange);
|
|
27
24
|
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function ThemeToggle() {
|
|
28
|
+
const t = useTranslations("_global_ui");
|
|
29
|
+
const theme = useSyncExternalStore(
|
|
30
|
+
subscribeToThemeChange,
|
|
31
|
+
getThemeSnapshot,
|
|
32
|
+
getServerThemeSnapshot,
|
|
33
|
+
);
|
|
34
|
+
const isDark = theme === "dark";
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
setIsDark(true);
|
|
36
|
-
} else if (theme === "light") {
|
|
37
|
-
document.documentElement.classList.add("light");
|
|
38
|
-
document.documentElement.classList.remove("dark");
|
|
39
|
-
setIsDark(false);
|
|
40
|
-
}
|
|
41
|
-
}, []);
|
|
36
|
+
const applyTheme = (dark: boolean) => {
|
|
37
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
38
|
+
document.documentElement.classList.toggle("light", !dark);
|
|
39
|
+
localStorage.setItem("theme", dark ? "dark" : "light");
|
|
40
|
+
window.dispatchEvent(new Event(themeChangeEvent));
|
|
41
|
+
};
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
44
|
<Button
|
|
45
|
-
onClick={
|
|
46
|
-
className="
|
|
47
|
-
aria-label="
|
|
45
|
+
onClick={() => applyTheme(!isDark)}
|
|
46
|
+
className="rounded-full p-0 dark:hover:bg-white/10 light:hover:bg-black/10 transition-colors"
|
|
47
|
+
aria-label={t("toggle_theme")}
|
|
48
|
+
variant="ghost"
|
|
49
|
+
size="icon"
|
|
48
50
|
type="button"
|
|
49
51
|
>
|
|
50
|
-
{isDark ?
|
|
52
|
+
{isDark ? (
|
|
53
|
+
<Sun className="h-5 w-5 text-white" />
|
|
54
|
+
) : (
|
|
55
|
+
<Moon className="h-5 w-5 text-black" />
|
|
56
|
+
)}
|
|
51
57
|
</Button>
|
|
52
58
|
);
|
|
53
59
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
"use client";
|
|
1
|
+
"use client";
|
|
2
2
|
import { Link } from "@/lib/i18n/navigation";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import LogoutButton from "../
|
|
4
|
+
import LogoutButton from "../dashboard/LogoutButton";
|
|
5
5
|
|
|
6
6
|
const navLinks = [
|
|
7
|
-
{ href: "/
|
|
8
|
-
{ href: "/
|
|
9
|
-
{ href: "/
|
|
7
|
+
{ href: "/dashboard", labelKey: "user_nav.links.dashboard" },
|
|
8
|
+
{ href: "/settings", labelKey: "user_nav.links.settings" },
|
|
9
|
+
{ href: "/userInfo", labelKey: "user_nav.links.user_info" },
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
const UserNav = () => {
|
|
@@ -18,7 +18,7 @@ const UserNav = () => {
|
|
|
18
18
|
<li key={link.href}>
|
|
19
19
|
<Link
|
|
20
20
|
href={link.href}
|
|
21
|
-
className="inline-block rounded px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-
|
|
21
|
+
className="inline-block rounded px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-stone-600 hover:bg-stone-50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-500"
|
|
22
22
|
>
|
|
23
23
|
{t(link.labelKey)}
|
|
24
24
|
</Link>
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// src/ui/_home/page-ui.tsx
|
|
2
|
-
|
|
2
|
+
import { getTranslations } from "next-intl/server";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export default function HomePageUI() {
|
|
7
|
-
const t = useTranslations("_home");
|
|
4
|
+
export default async function HomePageUI() {
|
|
5
|
+
const t = await getTranslations("_home");
|
|
8
6
|
|
|
9
7
|
return (
|
|
10
8
|
<>
|
|
11
|
-
<section className="pt-32 pb-20 px-4 sm:px-6 lg:px-8
|
|
9
|
+
<section className="pt-32 pb-20 px-4 sm:px-6 lg:px-8">
|
|
12
10
|
<div className="max-w-7xl mx-auto">
|
|
13
11
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
14
12
|
<h1 className="text-5xl lg:text-6xl font-bold mb-6 leading-tight">
|
|
@@ -18,7 +16,7 @@ export default function HomePageUI() {
|
|
|
18
16
|
),
|
|
19
17
|
})}
|
|
20
18
|
</h1>
|
|
21
|
-
<p className="text-lg
|
|
19
|
+
<p className="text-lg">{t("description")}</p>
|
|
22
20
|
</div>
|
|
23
21
|
</div>
|
|
24
22
|
</section>
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
// src/ui/
|
|
2
|
-
"use client";
|
|
1
|
+
// src/ui/dashboard/LogoutButton.tsx
|
|
2
|
+
"use client";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useLocale } from "next-intl";
|
|
5
|
+
import { signOut } from "next-auth/react";
|
|
4
6
|
import { Button } from "@/ui/_global/Button";
|
|
5
7
|
import { LogOut } from "lucide-react";
|
|
6
|
-
import { disconnect } from "@/lib/auth/disconnect";
|
|
7
8
|
|
|
8
9
|
const LogoutButton = () => {
|
|
9
|
-
const t = useTranslations("
|
|
10
|
+
const t = useTranslations("dashboard");
|
|
11
|
+
const locale = useLocale();
|
|
12
|
+
|
|
10
13
|
const handleLogout = () => {
|
|
11
|
-
|
|
12
|
-
window.location.href = "/";
|
|
13
|
-
});
|
|
14
|
+
void signOut({ redirectTo: `/${locale}` });
|
|
14
15
|
};
|
|
16
|
+
|
|
15
17
|
return (
|
|
16
18
|
<Button
|
|
17
19
|
variant="ghost"
|
|
@@ -3,11 +3,11 @@ import React from "react";
|
|
|
3
3
|
export default function StatsCard({ t }: { t: (key: string) => string }) {
|
|
4
4
|
return (
|
|
5
5
|
<div className="rounded border bg-white p-6 shadow-sm">
|
|
6
|
-
<h2 className="text-lg font-semibold mb-2">
|
|
6
|
+
<h2 className="text-lg font-semibold mb-2">{t("widgets.stats.title")}</h2>
|
|
7
7
|
<ul className="text-sm text-gray-600 space-y-1">
|
|
8
|
-
<li
|
|
9
|
-
<li
|
|
10
|
-
<li
|
|
8
|
+
<li>{t("widgets.stats.views")}: 123</li>
|
|
9
|
+
<li>{t("widgets.stats.likes")}: 45</li>
|
|
10
|
+
<li>{t("widgets.stats.comments")}: 7</li>
|
|
11
11
|
</ul>
|
|
12
12
|
</div>
|
|
13
13
|
);
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// src/ui/
|
|
2
|
-
|
|
3
|
-
import { useTranslations } from "next-intl";
|
|
1
|
+
// src/ui/dashboard/page-ui.tsx
|
|
2
|
+
import { getTranslations } from "next-intl/server";
|
|
4
3
|
import BackButton from "@/ui/_global/BackButton";
|
|
5
|
-
import WelcomeCard from "@/ui/
|
|
6
|
-
import StatsCard from "@/ui/
|
|
4
|
+
import WelcomeCard from "@/ui/dashboard/WelcomeCard";
|
|
5
|
+
import StatsCard from "@/ui/dashboard/StatsCard";
|
|
7
6
|
|
|
8
|
-
export default function Dashboard() {
|
|
9
|
-
const t =
|
|
7
|
+
export default async function Dashboard() {
|
|
8
|
+
const t = await getTranslations("dashboard");
|
|
10
9
|
return (
|
|
11
10
|
<>
|
|
12
11
|
<div className="flex items-center justify-between mb-6">
|
|
13
12
|
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
14
13
|
<BackButton />
|
|
15
14
|
</div>
|
|
16
|
-
<p className="
|
|
15
|
+
<p className="mb-8">{t("description")}</p>
|
|
17
16
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
18
17
|
<WelcomeCard t={t} />
|
|
19
18
|
<StatsCard t={t} />
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/ui/login/page-ui.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useLocale } from "next-intl";
|
|
6
|
+
import { signIn } from "next-auth/react";
|
|
7
|
+
import { siGoogle } from "simple-icons";
|
|
8
|
+
|
|
9
|
+
export default function LoginPageUI() {
|
|
10
|
+
const t = useTranslations("login");
|
|
11
|
+
const locale = useLocale();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<section className="py-8 px-4 max-w-3xl mx-auto">
|
|
15
|
+
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
16
|
+
<p className="mt-2">{t("description")}</p>
|
|
17
|
+
<button
|
|
18
|
+
onClick={() => signIn("google", { redirectTo: `/${locale}/dashboard` })}
|
|
19
|
+
className="mt-6 w-full py-2 px-4 rounded bg-[rgb(var(--background))] font-semibold hover:bg-stone-700 transition"
|
|
20
|
+
>
|
|
21
|
+
<svg
|
|
22
|
+
role="img"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
width="25"
|
|
25
|
+
height="25"
|
|
26
|
+
aria-label={siGoogle.title}
|
|
27
|
+
fill={`#${siGoogle.hex}`}
|
|
28
|
+
className="inline-block mr-4"
|
|
29
|
+
>
|
|
30
|
+
<path d={siGoogle.path} />
|
|
31
|
+
</svg>
|
|
32
|
+
{t("login_with_google")}
|
|
33
|
+
</button>
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/ui/register/page-ui.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useLocale } from "next-intl";
|
|
6
|
+
import { signIn } from "next-auth/react";
|
|
7
|
+
import { siGoogle } from "simple-icons";
|
|
8
|
+
|
|
9
|
+
export default function RegisterPageUI() {
|
|
10
|
+
const t = useTranslations("register");
|
|
11
|
+
const locale = useLocale();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<section className="py-8 px-4 max-w-md mx-auto">
|
|
15
|
+
<div className="flex items-center justify-between mb-6">
|
|
16
|
+
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
17
|
+
</div>
|
|
18
|
+
<p className="mb-6">{t("description")}</p>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => signIn("google", { redirectTo: `/${locale}/dashboard` })}
|
|
21
|
+
className="w-full py-2 px-4 rounded font-semibold bg-[rgb(var(--background))] hover:bg-[rgb(var(--secondary))] transition"
|
|
22
|
+
>
|
|
23
|
+
<svg
|
|
24
|
+
role="img"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
width="25"
|
|
27
|
+
height="25"
|
|
28
|
+
aria-label={siGoogle.title}
|
|
29
|
+
fill={`#${siGoogle.hex}`}
|
|
30
|
+
className="inline-block mr-4"
|
|
31
|
+
>
|
|
32
|
+
<path d={siGoogle.path} />
|
|
33
|
+
</svg>
|
|
34
|
+
{t("register_with_google")}
|
|
35
|
+
</button>
|
|
36
|
+
</section>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/ui/settings/page-ui.tsx
|
|
2
|
+
import { getTranslations } from "next-intl/server";
|
|
3
|
+
|
|
4
|
+
export default async function SettingsPageUI() {
|
|
5
|
+
const t = await getTranslations("settings");
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<section className="py-8 px-4 max-w-3xl mx-auto">
|
|
10
|
+
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
11
|
+
<p className="mt-2">{t("description")}</p>
|
|
12
|
+
</section>
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/ui/userInfo/page-ui.tsx
|
|
2
|
+
import { getTranslations } from "next-intl/server";
|
|
3
|
+
|
|
4
|
+
export default async function UserInfoPageUI() {
|
|
5
|
+
const t = await getTranslations("userInfo");
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<section className="py-8 px-4 max-w-3xl mx-auto">
|
|
10
|
+
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
|
11
|
+
<p className="mt-2">{t("description")}</p>
|
|
12
|
+
</section>
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|