create-next-pro-cli 0.1.28 → 0.1.29
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 +17 -3
- package/dist/bin.bun.js +16 -2
- package/dist/bin.node.js +16 -2
- package/dist/bin.node.js.map +1 -1
- package/package.json +1 -1
- package/templates/Projects/default/.env.example +8 -6
- package/templates/Projects/default/.github/workflows/quality.yml +40 -9
- package/templates/Projects/default/.gitignore.template +33 -15
- package/templates/Projects/default/README.md +38 -9
- package/templates/Projects/default/bun.lock +120 -134
- package/templates/Projects/default/messages/en/_home.json +9 -2
- package/templates/Projects/default/messages/en/settings.json +9 -1
- package/templates/Projects/default/messages/en/userInfo.json +6 -1
- package/templates/Projects/default/messages/fr/_home.json +9 -2
- package/templates/Projects/default/messages/fr/settings.json +9 -1
- package/templates/Projects/default/messages/fr/userInfo.json +6 -1
- package/templates/Projects/default/next.config.ts +11 -0
- package/templates/Projects/default/package.json +7 -1
- package/templates/Projects/default/playwright.config.ts +29 -6
- package/templates/Projects/default/pnpm-workspace.yaml +2 -0
- package/templates/Projects/default/scripts/audit.ts +4 -0
- package/templates/Projects/default/scripts/package-manager.ts +68 -0
- package/templates/Projects/default/src/app/[locale]/(public)/_home/page.tsx +2 -2
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +6 -1
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +6 -1
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/page.tsx +5 -2
- package/templates/Projects/default/src/app/[locale]/page.tsx +1 -4
- package/templates/Projects/default/src/lib/github/repository.ts +68 -0
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +10 -5
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +18 -8
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +5 -1
- package/templates/Projects/default/src/ui/_home/GitHubActions.tsx +57 -0
- package/templates/Projects/default/src/ui/_home/page-shell.tsx +16 -0
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +24 -13
- package/templates/Projects/default/src/ui/dashboard/LogoutButton.tsx +1 -1
- package/templates/Projects/default/src/ui/dashboard/StatsCard.tsx +2 -2
- package/templates/Projects/default/src/ui/dashboard/WelcomeCard.tsx +4 -2
- package/templates/Projects/default/src/ui/dashboard/page-ui.tsx +0 -2
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +45 -6
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +54 -7
- package/templates/Projects/default/tests/consumer/validate-template.ts +57 -48
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +47 -4
- package/templates/Projects/default/tests/unit/csp.test.ts +1 -1
- package/templates/Projects/default/tests/unit/github-repository.test.ts +45 -0
- package/templates/Projects/default/tests/unit/package-manager.test.ts +51 -0
- package/templates/Projects/default/tests/unit/template-baseline.test.ts +52 -0
- package/templates/Projects/default/tests/unit/user-nav.test.tsx +52 -0
- package/templates/Projects/default/tsconfig.json +2 -1
- package/templates/Projects/default/vitest.config.ts +10 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"title": "
|
|
3
|
-
"description": "
|
|
2
|
+
"title": "Welcome to your website",
|
|
3
|
+
"description": "Start developing right now.",
|
|
4
|
+
"readme": "Read the Doc",
|
|
5
|
+
"github": {
|
|
6
|
+
"project_credit": "This project was created with create-next-pro-cli",
|
|
7
|
+
"star": "Star",
|
|
8
|
+
"watch": "Watch",
|
|
9
|
+
"action_with_count": "{label} on GitHub, {count} total"
|
|
10
|
+
}
|
|
4
11
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "User Settings",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Manage your personal preferences.",
|
|
4
|
+
"language": {
|
|
5
|
+
"title": "Language",
|
|
6
|
+
"description": "Choose the language used by the application."
|
|
7
|
+
},
|
|
8
|
+
"theme": {
|
|
9
|
+
"title": "Theme",
|
|
10
|
+
"description": "Switch between the light and dark appearance."
|
|
11
|
+
}
|
|
4
12
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "User Information",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Information provided by your Google account.",
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"email": "Email address",
|
|
6
|
+
"profile_image_alt": "Google profile picture",
|
|
7
|
+
"profile_image_unavailable": "Google profile picture unavailable",
|
|
8
|
+
"unavailable": "Not provided"
|
|
4
9
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"title": "Bienvenue sur
|
|
3
|
-
"description": "Commencez votre
|
|
2
|
+
"title": "Bienvenue sur votre site",
|
|
3
|
+
"description": "Commencez votre développement dès maintenant.",
|
|
4
|
+
"readme": "Lire la documentation",
|
|
5
|
+
"github": {
|
|
6
|
+
"project_credit": "Ce projet est créé avec create-next-pro-cli",
|
|
7
|
+
"star": "Star",
|
|
8
|
+
"watch": "Watch",
|
|
9
|
+
"action_with_count": "{label} sur GitHub, {count} au total"
|
|
10
|
+
}
|
|
4
11
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Paramètres utilisateur",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Gérez vos préférences personnelles.",
|
|
4
|
+
"language": {
|
|
5
|
+
"title": "Langue",
|
|
6
|
+
"description": "Choisissez la langue utilisée par l’application."
|
|
7
|
+
},
|
|
8
|
+
"theme": {
|
|
9
|
+
"title": "Thème",
|
|
10
|
+
"description": "Basculez entre les apparences claire et sombre."
|
|
11
|
+
}
|
|
4
12
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Informations utilisateur",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Informations fournies par votre compte Google.",
|
|
4
|
+
"name": "Nom",
|
|
5
|
+
"email": "Adresse e-mail",
|
|
6
|
+
"profile_image_alt": "Photo de profil Google",
|
|
7
|
+
"profile_image_unavailable": "Photo de profil Google indisponible",
|
|
8
|
+
"unavailable": "Non renseigné"
|
|
4
9
|
}
|
|
@@ -8,6 +8,17 @@ const contentSecurityPolicy = createContentSecurityPolicy(
|
|
|
8
8
|
|
|
9
9
|
const nextConfig: NextConfig = {
|
|
10
10
|
poweredByHeader: false,
|
|
11
|
+
turbopack: {
|
|
12
|
+
root: process.cwd(),
|
|
13
|
+
},
|
|
14
|
+
images: {
|
|
15
|
+
remotePatterns: [
|
|
16
|
+
{
|
|
17
|
+
protocol: "https",
|
|
18
|
+
hostname: "lh3.googleusercontent.com",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
11
22
|
async headers() {
|
|
12
23
|
const headers = [
|
|
13
24
|
{
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"verify:rendering": "node --experimental-strip-types tests/rendering/verify-rendering.ts",
|
|
22
22
|
"check": "prettier --check . && eslint . && tsc --noEmit && vitest run && next build && node --experimental-strip-types tests/rendering/verify-rendering.ts",
|
|
23
23
|
"test:consumer": "node --experimental-strip-types tests/consumer/validate-template.ts",
|
|
24
|
-
"audit": "
|
|
24
|
+
"audit": "node --experimental-strip-types scripts/audit.ts"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@radix-ui/react-slot": "1.3.0",
|
|
@@ -52,5 +52,11 @@
|
|
|
52
52
|
},
|
|
53
53
|
"overrides": {
|
|
54
54
|
"postcss": "8.5.15"
|
|
55
|
+
},
|
|
56
|
+
"allowScripts": {
|
|
57
|
+
"@parcel/watcher": true,
|
|
58
|
+
"@swc/core": true,
|
|
59
|
+
"sharp": true,
|
|
60
|
+
"unrs-resolver": true
|
|
55
61
|
}
|
|
56
62
|
}
|
|
@@ -6,21 +6,44 @@ export default defineConfig({
|
|
|
6
6
|
fullyParallel: true,
|
|
7
7
|
retries: process.env.CI ? 2 : 0,
|
|
8
8
|
reporter: process.env.CI ? "github" : "list",
|
|
9
|
+
timeout: 60_000,
|
|
10
|
+
expect: {
|
|
11
|
+
timeout: 10_000,
|
|
12
|
+
},
|
|
9
13
|
use: {
|
|
10
14
|
baseURL: "http://127.0.0.1:3100",
|
|
11
15
|
trace: "retain-on-failure",
|
|
16
|
+
contextOptions: { reducedMotion: "reduce" },
|
|
12
17
|
},
|
|
13
|
-
projects: [
|
|
14
|
-
{ name: "desktop", use: { ...devices["Desktop Chrome"] } },
|
|
15
|
-
{ name: "mobile", use: { ...devices["Pixel 5"] } },
|
|
16
|
-
],
|
|
17
18
|
webServer: {
|
|
18
19
|
command: "next build && next start --hostname 127.0.0.1 --port 3100",
|
|
19
20
|
url: "http://127.0.0.1:3100/en",
|
|
20
|
-
reuseExistingServer: !process.env.CI,
|
|
21
21
|
env: {
|
|
22
|
-
AUTH_DISABLED: "true",
|
|
23
22
|
NEXT_PUBLIC_APP_URL: "http://127.0.0.1:3100",
|
|
23
|
+
AUTH_URL: "http://127.0.0.1:3100",
|
|
24
|
+
NEXTAUTH_URL: "http://127.0.0.1:3100",
|
|
25
|
+
AUTH_SECRET: "",
|
|
26
|
+
AUTH_GOOGLE_ID: "",
|
|
27
|
+
AUTH_GOOGLE_SECRET: "",
|
|
28
|
+
AUTH_DISABLED: "true",
|
|
24
29
|
},
|
|
30
|
+
reuseExistingServer: !process.env.CI,
|
|
31
|
+
timeout: 120_000,
|
|
25
32
|
},
|
|
33
|
+
projects: [
|
|
34
|
+
{
|
|
35
|
+
name: "desktop",
|
|
36
|
+
use: {
|
|
37
|
+
...devices["Desktop Chrome"],
|
|
38
|
+
contextOptions: { reducedMotion: "reduce" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "mobile",
|
|
43
|
+
use: {
|
|
44
|
+
...devices["Pixel 5"],
|
|
45
|
+
contextOptions: { reducedMotion: "reduce" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
26
49
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const packageManagers = ["bun", "npm", "pnpm"] as const;
|
|
4
|
+
|
|
5
|
+
export type PackageManager = (typeof packageManagers)[number];
|
|
6
|
+
|
|
7
|
+
function isPackageManager(value: string): value is PackageManager {
|
|
8
|
+
return packageManagers.includes(value as PackageManager);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolvePackageManager(
|
|
12
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
13
|
+
): PackageManager {
|
|
14
|
+
const explicit = env.CNP_PACKAGE_MANAGER?.trim();
|
|
15
|
+
if (explicit) {
|
|
16
|
+
if (isPackageManager(explicit)) return explicit;
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Unsupported package manager "${explicit}". Expected bun, npm, or pnpm.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const userAgent = env.npm_config_user_agent?.trim().split(/[\s/]/, 1)[0];
|
|
23
|
+
if (userAgent && isPackageManager(userAgent)) return userAgent;
|
|
24
|
+
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Unable to detect the package manager. Run the script through bun, npm, or pnpm, or set CNP_PACKAGE_MANAGER.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function packageManagerExecutable(
|
|
31
|
+
manager: PackageManager,
|
|
32
|
+
platform: NodeJS.Platform = process.platform,
|
|
33
|
+
): string {
|
|
34
|
+
return platform === "win32" && manager !== "bun" ? `${manager}.cmd` : manager;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function installArguments(manager: PackageManager): string[] {
|
|
38
|
+
if (manager === "bun") return ["install", "--frozen-lockfile"];
|
|
39
|
+
if (manager === "pnpm") return ["install", "--no-frozen-lockfile"];
|
|
40
|
+
return ["install"];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runPackageManager(
|
|
44
|
+
manager: PackageManager,
|
|
45
|
+
args: string[],
|
|
46
|
+
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const executable = packageManagerExecutable(manager);
|
|
50
|
+
const child = spawn(executable, args, {
|
|
51
|
+
cwd: options.cwd,
|
|
52
|
+
env: options.env ?? process.env,
|
|
53
|
+
shell: false,
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
});
|
|
56
|
+
child.once("error", reject);
|
|
57
|
+
child.once("exit", (code, signal) => {
|
|
58
|
+
if (code === 0) resolve();
|
|
59
|
+
else {
|
|
60
|
+
reject(
|
|
61
|
+
new Error(
|
|
62
|
+
`${manager} ${args.join(" ")} failed${signal ? ` with signal ${signal}` : ` with exit code ${code ?? "unknown"}`}.`,
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import GlobalHeader from "@/ui/_global/GlobalHeader";
|
|
3
|
-
import GlobalMain from "@/ui/_global/GlobalMain";
|
|
4
3
|
|
|
5
4
|
export default function PublicLayout({
|
|
6
5
|
children,
|
|
@@ -10,7 +9,7 @@ export default function PublicLayout({
|
|
|
10
9
|
return (
|
|
11
10
|
<>
|
|
12
11
|
<GlobalHeader hasSessionInitial={false} />
|
|
13
|
-
|
|
12
|
+
{children}
|
|
14
13
|
</>
|
|
15
14
|
);
|
|
16
15
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// src/app/[locale]/(public)/login/page.tsx
|
|
2
2
|
import LoginPageUI from "@/ui/login/page-ui";
|
|
3
|
+
import GlobalMain from "@/ui/_global/GlobalMain";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-static";
|
|
5
6
|
|
|
6
7
|
export default function LoginPage() {
|
|
7
|
-
return
|
|
8
|
+
return (
|
|
9
|
+
<GlobalMain>
|
|
10
|
+
<LoginPageUI />
|
|
11
|
+
</GlobalMain>
|
|
12
|
+
);
|
|
8
13
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// src/app/[locale]/(public)/register/page.tsx
|
|
2
2
|
import RegisterPageUI from "@/ui/register/page-ui";
|
|
3
|
+
import GlobalMain from "@/ui/_global/GlobalMain";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-static";
|
|
5
6
|
|
|
6
7
|
export default function RegisterPage() {
|
|
7
|
-
return
|
|
8
|
+
return (
|
|
9
|
+
<GlobalMain>
|
|
10
|
+
<RegisterPageUI />
|
|
11
|
+
</GlobalMain>
|
|
12
|
+
);
|
|
8
13
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { auth } from "@/auth";
|
|
1
2
|
import UserInfoPageUI from "@/ui/userInfo/page-ui";
|
|
2
3
|
|
|
3
|
-
export default function UserInfoPage() {
|
|
4
|
-
|
|
4
|
+
export default async function UserInfoPage() {
|
|
5
|
+
const session = await auth();
|
|
6
|
+
|
|
7
|
+
return <UserInfoPageUI user={session?.user} />;
|
|
5
8
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import HomePage from "@/app/[locale]/(public)/_home/page";
|
|
3
3
|
import GlobalHeader from "@/ui/_global/GlobalHeader";
|
|
4
|
-
import GlobalMain from "@/ui/_global/GlobalMain";
|
|
5
4
|
|
|
6
5
|
export default async function FallbackPage() {
|
|
7
6
|
return (
|
|
8
7
|
<>
|
|
9
8
|
<GlobalHeader hasSessionInitial={false} />
|
|
10
|
-
<
|
|
11
|
-
<HomePage />
|
|
12
|
-
</GlobalMain>
|
|
9
|
+
<HomePage />
|
|
13
10
|
</>
|
|
14
11
|
);
|
|
15
12
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const GITHUB_REPOSITORY_URL =
|
|
2
|
+
"https://github.com/Rising-Corporation/create-next-pro-cli";
|
|
3
|
+
export const GITHUB_README_URL = `${GITHUB_REPOSITORY_URL}#readme`;
|
|
4
|
+
|
|
5
|
+
const GITHUB_REPOSITORY_API_URL =
|
|
6
|
+
"https://api.github.com/repos/Rising-Corporation/create-next-pro-cli";
|
|
7
|
+
|
|
8
|
+
export type GitHubRepositoryStats = {
|
|
9
|
+
stars: number | null;
|
|
10
|
+
watchers: number | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const unavailableStats: GitHubRepositoryStats = {
|
|
14
|
+
stars: null,
|
|
15
|
+
watchers: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type GitHubRepositoryFetcher = (
|
|
19
|
+
url: string,
|
|
20
|
+
init: {
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
next: { revalidate: number };
|
|
23
|
+
},
|
|
24
|
+
) => Promise<Response>;
|
|
25
|
+
|
|
26
|
+
export function parseGitHubRepositoryStats(
|
|
27
|
+
value: unknown,
|
|
28
|
+
): GitHubRepositoryStats {
|
|
29
|
+
if (!value || typeof value !== "object") return unavailableStats;
|
|
30
|
+
|
|
31
|
+
const repository = value as Record<string, unknown>;
|
|
32
|
+
const stars = repository.stargazers_count;
|
|
33
|
+
const watchers = repository.subscribers_count;
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!Number.isInteger(stars) ||
|
|
37
|
+
(stars as number) < 0 ||
|
|
38
|
+
!Number.isInteger(watchers) ||
|
|
39
|
+
(watchers as number) < 0
|
|
40
|
+
) {
|
|
41
|
+
return unavailableStats;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
stars: stars as number,
|
|
46
|
+
watchers: watchers as number,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getGitHubRepositoryStats(
|
|
51
|
+
fetcher: GitHubRepositoryFetcher = fetch,
|
|
52
|
+
): Promise<GitHubRepositoryStats> {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetcher(GITHUB_REPOSITORY_API_URL, {
|
|
55
|
+
headers: {
|
|
56
|
+
Accept: "application/vnd.github+json",
|
|
57
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
58
|
+
},
|
|
59
|
+
next: { revalidate: 3600 },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) return unavailableStats;
|
|
63
|
+
|
|
64
|
+
return parseGitHubRepositoryStats(await response.json());
|
|
65
|
+
} catch {
|
|
66
|
+
return unavailableStats;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -2,14 +2,19 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
export default function GlobalMain({
|
|
4
4
|
children,
|
|
5
|
+
footer,
|
|
5
6
|
}: {
|
|
6
7
|
children: React.ReactNode;
|
|
8
|
+
footer?: React.ReactNode;
|
|
7
9
|
}) {
|
|
8
10
|
return (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
<div className="mt-0 flex min-h-screen flex-col bg-neutral-50 text-foreground font-sans hero-pattern">
|
|
12
|
+
<main className="flex flex-1 flex-col">
|
|
13
|
+
<div className="flex-1 flex flex-col justify-center items-stretch mx-auto w-full px-6 self-stretch">
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
</main>
|
|
17
|
+
{footer}
|
|
18
|
+
</div>
|
|
14
19
|
);
|
|
15
20
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
3
3
|
import { Moon, Sun } from "lucide-react";
|
|
4
4
|
import { Button } from "./Button";
|
|
5
|
-
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
6
6
|
|
|
7
7
|
const themeChangeEvent = "template-theme-change";
|
|
8
8
|
|
|
@@ -24,8 +24,16 @@ function subscribeToThemeChange(onStoreChange: () => void) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function applyTheme(dark: boolean) {
|
|
28
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
29
|
+
document.documentElement.classList.toggle("light", !dark);
|
|
30
|
+
localStorage.setItem("theme", dark ? "dark" : "light");
|
|
31
|
+
window.dispatchEvent(new Event(themeChangeEvent));
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
export default function ThemeToggle() {
|
|
28
35
|
const t = useTranslations("_global_ui");
|
|
36
|
+
const locale = useLocale();
|
|
29
37
|
const theme = useSyncExternalStore(
|
|
30
38
|
subscribeToThemeChange,
|
|
31
39
|
getThemeSnapshot,
|
|
@@ -33,12 +41,14 @@ export default function ThemeToggle() {
|
|
|
33
41
|
);
|
|
34
42
|
const isDark = theme === "dark";
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const storedTheme = localStorage.getItem("theme");
|
|
46
|
+
const shouldUseDark = storedTheme
|
|
47
|
+
? storedTheme === "dark"
|
|
48
|
+
: window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
49
|
+
|
|
50
|
+
applyTheme(shouldUseDark);
|
|
51
|
+
}, [locale]);
|
|
42
52
|
|
|
43
53
|
return (
|
|
44
54
|
<Button
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Link } from "@/lib/i18n/navigation";
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
4
|
import LogoutButton from "../dashboard/LogoutButton";
|
|
5
|
+
import ThemeToggle from "./ThemeToggle";
|
|
5
6
|
|
|
6
7
|
const navLinks = [
|
|
7
8
|
{ href: "/dashboard", labelKey: "user_nav.links.dashboard" },
|
|
@@ -18,12 +19,15 @@ const UserNav = () => {
|
|
|
18
19
|
<li key={link.href}>
|
|
19
20
|
<Link
|
|
20
21
|
href={link.href}
|
|
21
|
-
className="inline-block rounded px-3 py-1.5 text-sm font-medium text-
|
|
22
|
+
className="inline-block rounded px-3 py-1.5 text-sm font-medium dark:text-stone-100 light:text-stone-900 hover:text-stone-600 hover:bg-stone-50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-500"
|
|
22
23
|
>
|
|
23
24
|
{t(link.labelKey)}
|
|
24
25
|
</Link>
|
|
25
26
|
</li>
|
|
26
27
|
))}
|
|
28
|
+
<li>
|
|
29
|
+
<ThemeToggle />
|
|
30
|
+
</li>
|
|
27
31
|
<li>
|
|
28
32
|
<LogoutButton />
|
|
29
33
|
</li>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Eye, Star } from "lucide-react";
|
|
2
|
+
import { getTranslations } from "next-intl/server";
|
|
3
|
+
import { Button } from "@/ui/_global/Button";
|
|
4
|
+
import {
|
|
5
|
+
GITHUB_REPOSITORY_URL,
|
|
6
|
+
getGitHubRepositoryStats,
|
|
7
|
+
} from "@/lib/github/repository";
|
|
8
|
+
|
|
9
|
+
export default async function GitHubActions() {
|
|
10
|
+
const [t, stats] = await Promise.all([
|
|
11
|
+
getTranslations("_home.github"),
|
|
12
|
+
getGitHubRepositoryStats(),
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const actions = [
|
|
16
|
+
{
|
|
17
|
+
label: t("star"),
|
|
18
|
+
count: stats.stars,
|
|
19
|
+
icon: Star,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: t("watch"),
|
|
23
|
+
count: stats.watchers,
|
|
24
|
+
icon: Eye,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="mt-12 text-center">
|
|
30
|
+
<p className="mb-4">{t("project_credit")}</p>
|
|
31
|
+
<div className="flex flex-wrap items-center justify-center gap-4">
|
|
32
|
+
{actions.map(({ label, count, icon: Icon }) => (
|
|
33
|
+
<Button key={label} asChild variant="outline">
|
|
34
|
+
<a
|
|
35
|
+
href={GITHUB_REPOSITORY_URL}
|
|
36
|
+
target="_blank"
|
|
37
|
+
rel="noreferrer"
|
|
38
|
+
aria-label={
|
|
39
|
+
count === null
|
|
40
|
+
? label
|
|
41
|
+
: t("action_with_count", { label, count })
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<Icon className="mr-2 h-4 w-4" aria-hidden="true" />
|
|
45
|
+
<span>{label}</span>
|
|
46
|
+
{count !== null && (
|
|
47
|
+
<span className="ml-2 border-l border-current pl-2 tabular-nums">
|
|
48
|
+
{count}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</a>
|
|
52
|
+
</Button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import GlobalMain from "@/ui/_global/GlobalMain";
|
|
2
|
+
import HomePageUI from "@/ui/_home/page-ui";
|
|
3
|
+
|
|
4
|
+
const footer = (
|
|
5
|
+
<footer className="border-t px-4 py-6 text-center text-sm text-gray-600 dark:text-gray-300">
|
|
6
|
+
<p>Empowered by Rising Corporation</p>
|
|
7
|
+
</footer>
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export default function HomePageShell() {
|
|
11
|
+
return (
|
|
12
|
+
<GlobalMain footer={footer}>
|
|
13
|
+
<HomePageUI />
|
|
14
|
+
</GlobalMain>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
// src/ui/_home/page-ui.tsx
|
|
2
2
|
import { getTranslations } from "next-intl/server";
|
|
3
|
+
import { ArrowRight } from "lucide-react";
|
|
4
|
+
import { GITHUB_README_URL } from "@/lib/github/repository";
|
|
5
|
+
import GitHubActions from "@/ui/_home/GitHubActions";
|
|
3
6
|
|
|
4
7
|
export default async function HomePageUI() {
|
|
5
8
|
const t = await getTranslations("_home");
|
|
6
9
|
|
|
7
10
|
return (
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
<div className="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
})}
|
|
18
|
-
</h1>
|
|
11
|
+
<section className="pt-32 pb-20 px-4 sm:px-6 lg:px-8">
|
|
12
|
+
<div className="max-w-7xl mx-auto">
|
|
13
|
+
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
14
|
+
<h1 className="text-5xl lg:text-6xl font-bold mb-6 leading-tight">
|
|
15
|
+
{t.rich("title", {
|
|
16
|
+
span: (chunks) => <span className="gradient-text">{chunks}</span>,
|
|
17
|
+
})}
|
|
18
|
+
</h1>
|
|
19
|
+
<div>
|
|
19
20
|
<p className="text-lg">{t("description")}</p>
|
|
21
|
+
<a
|
|
22
|
+
href={GITHUB_README_URL}
|
|
23
|
+
target="_blank"
|
|
24
|
+
rel="noreferrer"
|
|
25
|
+
className="mt-4 inline-flex items-center gap-2 font-medium underline underline-offset-4 hover:text-stone-600"
|
|
26
|
+
>
|
|
27
|
+
{t("readme")}
|
|
28
|
+
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
|
29
|
+
</a>
|
|
20
30
|
</div>
|
|
21
31
|
</div>
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
<GitHubActions />
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
24
35
|
);
|
|
25
36
|
}
|
|
@@ -19,7 +19,7 @@ const LogoutButton = () => {
|
|
|
19
19
|
variant="ghost"
|
|
20
20
|
size="sm"
|
|
21
21
|
onClick={handleLogout}
|
|
22
|
-
className="text-gray-400 hover:text-black
|
|
22
|
+
className="text-gray-400 hover:text-black dark:hover:text-white glass-effect"
|
|
23
23
|
>
|
|
24
24
|
<LogOut className="h-4 w-4 sm:mr-2" />
|
|
25
25
|
<span className="hidden sm:inline">{t("logout")}</span>
|
|
@@ -2,9 +2,9 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
export default function StatsCard({ t }: { t: (key: string) => string }) {
|
|
4
4
|
return (
|
|
5
|
-
<div className="rounded border bg-white p-6 shadow-sm">
|
|
5
|
+
<div className="rounded border bg-white p-6 shadow-sm dark:bg-black">
|
|
6
6
|
<h2 className="text-lg font-semibold mb-2">{t("widgets.stats.title")}</h2>
|
|
7
|
-
<ul className="text-sm text-gray-600 space-y-1">
|
|
7
|
+
<ul className="text-sm text-gray-600 space-y-1 dark:text-gray-300">
|
|
8
8
|
<li>{t("widgets.stats.views")}: 123</li>
|
|
9
9
|
<li>{t("widgets.stats.likes")}: 45</li>
|
|
10
10
|
<li>{t("widgets.stats.comments")}: 7</li>
|