@clubedge/create-clubedge-app 0.2.0 → 0.3.0

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +50 -35
  3. package/bin/create.mjs +2 -466
  4. package/dist/cli/args.js +49 -0
  5. package/dist/cli/ui.js +85 -0
  6. package/dist/core/customize.js +56 -0
  7. package/dist/core/execute.js +60 -0
  8. package/dist/core/manifest.js +81 -0
  9. package/dist/core/plan.js +67 -0
  10. package/dist/core/source.js +107 -0
  11. package/dist/index.js +125 -0
  12. package/dist/package-info.js +8 -0
  13. package/dist/steps/git.js +11 -0
  14. package/dist/steps/install.js +19 -0
  15. package/dist/utils/fs.js +97 -0
  16. package/dist/utils/names.js +14 -0
  17. package/dist/utils/process.js +49 -0
  18. package/package.json +21 -5
  19. package/template/.dockerignore +8 -0
  20. package/template/.env.example +25 -0
  21. package/template/.gitattributes +6 -0
  22. package/template/.github/ISSUE_TEMPLATE/bug_report.yml +35 -0
  23. package/template/.github/ISSUE_TEMPLATE/config.yml +8 -0
  24. package/template/.github/ISSUE_TEMPLATE/feature_request.yml +24 -0
  25. package/template/.github/pull_request_template.md +20 -0
  26. package/template/.github/workflows/ci.yml +49 -0
  27. package/template/.prettierignore +1 -0
  28. package/template/.prettierrc.json +1 -0
  29. package/template/AGENTS.md +31 -0
  30. package/template/CODE_OF_CONDUCT.md +22 -0
  31. package/template/CONTRIBUTING.md +53 -0
  32. package/template/Dockerfile +32 -0
  33. package/template/LICENSE +201 -0
  34. package/template/NOTICE +6 -0
  35. package/template/PUBLISHING.md +27 -0
  36. package/template/README.md +157 -0
  37. package/template/SECURITY.md +15 -0
  38. package/template/SETUP.md +174 -0
  39. package/template/SUPPORT.md +8 -0
  40. package/template/_gitignore +13 -0
  41. package/template/apps/web/AGENTS.md +9 -0
  42. package/template/apps/web/components.json +21 -0
  43. package/template/apps/web/e2e/auth.spec.ts +37 -0
  44. package/template/apps/web/e2e/home.spec.ts +15 -0
  45. package/template/apps/web/next-env.d.ts +7 -0
  46. package/template/apps/web/next.config.ts +32 -0
  47. package/template/apps/web/package.json +33 -0
  48. package/template/apps/web/postcss.config.mjs +3 -0
  49. package/template/apps/web/public/.gitkeep +0 -0
  50. package/template/apps/web/public/.well-known/logo.svg +1 -0
  51. package/template/apps/web/src/app/actions/auth.ts +85 -0
  52. package/template/apps/web/src/app/api/health/route.ts +11 -0
  53. package/template/apps/web/src/app/auth/callback/route.ts +21 -0
  54. package/template/apps/web/src/app/dashboard/_components/account-button.tsx +34 -0
  55. package/template/apps/web/src/app/dashboard/_components/app-sidebar.tsx +91 -0
  56. package/template/apps/web/src/app/dashboard/page.tsx +352 -0
  57. package/template/apps/web/src/app/favicon.ico +0 -0
  58. package/template/apps/web/src/app/layout.tsx +36 -0
  59. package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +88 -0
  60. package/template/apps/web/src/app/login/page.tsx +149 -0
  61. package/template/apps/web/src/app/page.tsx +229 -0
  62. package/template/apps/web/src/app/theme-provider.tsx +43 -0
  63. package/template/apps/web/src/app/theme-toggle.tsx +23 -0
  64. package/template/apps/web/src/components/branding/clubedge-mark.tsx +25 -0
  65. package/template/apps/web/src/components/landing/copy-command.tsx +48 -0
  66. package/template/apps/web/src/config/site.json +11 -0
  67. package/template/apps/web/src/config/site.ts +20 -0
  68. package/template/apps/web/src/env/client.ts +22 -0
  69. package/template/apps/web/src/env/server.test.ts +35 -0
  70. package/template/apps/web/src/env/server.ts +45 -0
  71. package/template/apps/web/src/lib/login-url.test.ts +16 -0
  72. package/template/apps/web/src/lib/login-url.ts +16 -0
  73. package/template/apps/web/src/lib/utils.ts +1 -0
  74. package/template/apps/web/src/proxy.ts +31 -0
  75. package/template/apps/web/src/server/auth.ts +69 -0
  76. package/template/apps/web/src/server/cache.ts +38 -0
  77. package/template/apps/web/src/server/db.ts +16 -0
  78. package/template/apps/web/src/server/storage.ts +39 -0
  79. package/template/apps/web/src/server/users.ts +17 -0
  80. package/template/apps/web/tsconfig.json +36 -0
  81. package/template/clubedge.template.json +13 -0
  82. package/template/docker-compose.yml +8 -0
  83. package/template/eslint.config.mjs +63 -0
  84. package/template/package.json +47 -0
  85. package/template/packages/auth/package.json +23 -0
  86. package/template/packages/auth/src/index.ts +9 -0
  87. package/template/packages/auth/src/supabase.test.ts +126 -0
  88. package/template/packages/auth/src/supabase.ts +65 -0
  89. package/template/packages/auth/src/types.ts +59 -0
  90. package/template/packages/auth/tsconfig.json +4 -0
  91. package/template/packages/cache/package.json +20 -0
  92. package/template/packages/cache/src/index.test.ts +67 -0
  93. package/template/packages/cache/src/index.ts +19 -0
  94. package/template/packages/cache/src/rate-limit.test.ts +52 -0
  95. package/template/packages/cache/src/rate-limit.ts +74 -0
  96. package/template/packages/cache/src/redis.ts +106 -0
  97. package/template/packages/cache/tsconfig.json +4 -0
  98. package/template/packages/core/package.json +17 -0
  99. package/template/packages/core/src/client-ip.test.ts +19 -0
  100. package/template/packages/core/src/client-ip.ts +11 -0
  101. package/template/packages/core/src/errors.test.ts +20 -0
  102. package/template/packages/core/src/errors.ts +20 -0
  103. package/template/packages/core/src/index.ts +4 -0
  104. package/template/packages/core/src/redirect.test.ts +27 -0
  105. package/template/packages/core/src/redirect.ts +20 -0
  106. package/template/packages/core/src/result.ts +10 -0
  107. package/template/packages/core/tsconfig.json +4 -0
  108. package/template/packages/db/drizzle/0000_initial.sql +10 -0
  109. package/template/packages/db/drizzle/meta/_journal.json +13 -0
  110. package/template/packages/db/drizzle.config.ts +9 -0
  111. package/template/packages/db/package.json +30 -0
  112. package/template/packages/db/scripts/load-env.ts +14 -0
  113. package/template/packages/db/scripts/migrate.ts +11 -0
  114. package/template/packages/db/scripts/seed.ts +14 -0
  115. package/template/packages/db/src/client.ts +22 -0
  116. package/template/packages/db/src/index.ts +4 -0
  117. package/template/packages/db/src/schema/index.ts +1 -0
  118. package/template/packages/db/src/schema/users.ts +14 -0
  119. package/template/packages/db/src/users.ts +16 -0
  120. package/template/packages/db/tsconfig.json +4 -0
  121. package/template/packages/storage/package.json +24 -0
  122. package/template/packages/storage/src/index.ts +1 -0
  123. package/template/packages/storage/src/s3.test.ts +86 -0
  124. package/template/packages/storage/src/s3.ts +86 -0
  125. package/template/packages/storage/src/supabase.ts +69 -0
  126. package/template/packages/storage/src/types.ts +18 -0
  127. package/template/packages/storage/tsconfig.json +4 -0
  128. package/template/packages/ui/components.json +21 -0
  129. package/template/packages/ui/package.json +32 -0
  130. package/template/packages/ui/src/components/badge.tsx +48 -0
  131. package/template/packages/ui/src/components/breadcrumb.tsx +102 -0
  132. package/template/packages/ui/src/components/button.tsx +57 -0
  133. package/template/packages/ui/src/components/card.tsx +87 -0
  134. package/template/packages/ui/src/components/input.tsx +19 -0
  135. package/template/packages/ui/src/components/label.tsx +19 -0
  136. package/template/packages/ui/src/components/separator.tsx +20 -0
  137. package/template/packages/ui/src/components/sheet.tsx +125 -0
  138. package/template/packages/ui/src/components/sidebar.tsx +692 -0
  139. package/template/packages/ui/src/components/skeleton.tsx +13 -0
  140. package/template/packages/ui/src/components/switch.tsx +31 -0
  141. package/template/packages/ui/src/components/table.tsx +88 -0
  142. package/template/packages/ui/src/components/tooltip.tsx +53 -0
  143. package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
  144. package/template/packages/ui/src/lib/utils.ts +1 -0
  145. package/template/packages/ui/src/styles/globals.css +126 -0
  146. package/template/packages/ui/tsconfig.json +19 -0
  147. package/template/playwright.config.ts +19 -0
  148. package/template/pnpm-lock.yaml +3624 -0
  149. package/template/pnpm-workspace.yaml +3 -0
  150. package/template/scripts/start-standalone.mjs +37 -0
  151. package/template/tsconfig.base.json +16 -0
  152. package/template/turbo.json +17 -0
  153. package/template/vitest.config.ts +8 -0
  154. package/template.lock.json +142 -0
@@ -0,0 +1,37 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import site from "../src/config/site.json";
3
+
4
+ // The browser suite runs without Supabase credentials, so these checks cover the
5
+ // unconfigured mode: pages stay explorable and redirects stay on this origin.
6
+
7
+ test("dashboard stays explorable while authentication is not configured", async ({ page }) => {
8
+ await page.goto("/dashboard");
9
+ await expect(page).toHaveURL(/\/dashboard$/);
10
+ await expect(
11
+ page.getByRole("heading", { name: "A better place to start building." }),
12
+ ).toBeVisible();
13
+ await expect(page.getByRole("link", { name: "Sign in" }).first()).toBeVisible();
14
+ });
15
+
16
+ test("login keeps a same-origin destination and drops external ones", async ({ page }) => {
17
+ await page.goto("/login?next=%2Fprojects%3Ftab%3Da");
18
+ await expect(page.locator('input[name="next"]')).toHaveValue("/projects?tab=a");
19
+ await expect(page.getByRole("link", { name: "Create one" })).toHaveAttribute(
20
+ "href",
21
+ "/login?mode=signup&next=%2Fprojects%3Ftab%3Da",
22
+ );
23
+
24
+ await page.goto("/login?next=https%3A%2F%2Fevil.example");
25
+ await expect(page.locator('input[name="next"]')).toHaveValue("/dashboard");
26
+ });
27
+
28
+ test("login explains the rate limit error", async ({ page }) => {
29
+ await page.goto("/login?error=rate-limited");
30
+ await expect(page.getByRole("alert")).toContainText("Too many attempts");
31
+ });
32
+
33
+ test("health endpoint reports the configured service id", async ({ request }) => {
34
+ const health = await request.get("/api/health");
35
+ expect(health.ok()).toBeTruthy();
36
+ expect(await health.json()).toMatchObject({ status: "ok", service: site.serviceId });
37
+ });
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("starter landing page loads with setup guidance and a health endpoint", async ({
4
+ page,
5
+ request,
6
+ }) => {
7
+ await page.goto("/");
8
+ await expect(
9
+ page.getByRole("heading", { name: "Start with the foundation. Build what matters." }),
10
+ ).toBeVisible();
11
+ await expect(page.getByRole("heading", { name: "Up and running in three steps" })).toBeVisible();
12
+ const health = await request.get("/api/health");
13
+ expect(health.ok()).toBeTruthy();
14
+ expect((await health.json()).status).toBe("ok");
15
+ });
@@ -0,0 +1,7 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+ import "./.next/dev/types/root-params.d.ts";
5
+
6
+ // NOTE: This file should not be edited
7
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,32 @@
1
+ import type { NextConfig } from "next";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const workspaceRoot = fileURLToPath(new URL("../..", import.meta.url));
5
+
6
+ const securityHeaders = [
7
+ { key: "X-Content-Type-Options", value: "nosniff" },
8
+ { key: "X-Frame-Options", value: "DENY" },
9
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
10
+ { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
11
+ { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
12
+ ];
13
+
14
+ const nextConfig: NextConfig = {
15
+ poweredByHeader: false,
16
+ output: "standalone",
17
+ outputFileTracingRoot: workspaceRoot,
18
+ // Workspace packages ship TypeScript source and are compiled by the app.
19
+ transpilePackages: [
20
+ "@clubedge/auth",
21
+ "@clubedge/cache",
22
+ "@clubedge/core",
23
+ "@clubedge/db",
24
+ "@clubedge/storage",
25
+ "@clubedge/ui",
26
+ ],
27
+ async headers() {
28
+ return [{ source: "/:path*", headers: securityHeaders }];
29
+ },
30
+ };
31
+
32
+ export default nextConfig;
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@clubedge/web",
3
+ "version": "0.3.0",
4
+ "private": true,
5
+ "license": "Apache-2.0",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "node ../../scripts/start-standalone.mjs",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@clubedge/auth": "workspace:*",
14
+ "@clubedge/cache": "workspace:*",
15
+ "@clubedge/core": "workspace:*",
16
+ "@clubedge/db": "workspace:*",
17
+ "@clubedge/storage": "workspace:*",
18
+ "@clubedge/ui": "workspace:*",
19
+ "cn": "^0.4.0",
20
+ "lucide-react": "^0.468.0",
21
+ "next": "^16.1.0",
22
+ "next-themes": "^0.4.6",
23
+ "react": "^19.2.0",
24
+ "react-dom": "^19.2.0",
25
+ "zod": "^4.1.5"
26
+ },
27
+ "devDependencies": {
28
+ "@tailwindcss/postcss": "^4.1.13",
29
+ "@types/react": "^19.1.13",
30
+ "@types/react-dom": "^19.1.9",
31
+ "tailwindcss": "^4.1.13"
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ const config = { plugins: { "@tailwindcss/postcss": {} } };
2
+
3
+ export default config;
File without changes
@@ -0,0 +1 @@
1
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1{fill:#2563eb;}.cls-2,.cls-3{fill:#fff;}.cls-2{fill-rule:evenodd;}</style></defs><rect class="cls-1" width="512" height="512"/><path class="cls-2" d="M364.36,283a359.51,359.51,0,0,0-43.5-10.18,364.29,364.29,0,0,1,5.24-55.17l30-15.13,20.83,5.63v34.6A43.79,43.79,0,0,0,364.36,283Zm-66.88-13.4a395.53,395.53,0,0,0-82.94,0,390.43,390.43,0,0,0-3-39L256,253l44.45-22.42A390.43,390.43,0,0,0,297.48,269.6Zm-106.32,3.22c-39.75,6.75-76,19.56-106.55,36.9a175.47,175.47,0,0,1-2.52-29.64,173.14,173.14,0,0,1,31-99.15l72.81,36.72a364.29,364.29,0,0,1,5.24,55.17Zm230.41,60.63A174.34,174.34,0,0,1,352.17,425c-17.72-36.5-28.9-80.53-31-128.34,37.9,6.89,72.13,19.67,100.42,36.78Zm-89.78,103.2a174.19,174.19,0,0,1-151.57,0c20-40.75,32.48-90.09,34.24-143.46a373.23,373.23,0,0,1,83.1,0C299.32,346.56,311.75,395.9,331.79,436.65ZM159.85,425a174.36,174.36,0,0,1-69.41-91.56c28.3-17.11,62.53-29.89,100.42-36.78-2.11,47.81-13.3,91.84-31,128.34Z"/><path class="cls-2" d="M256,58l173,84.17-33.81,17.05L280.05,128.07c-6.92-4.44-16-6.2-24-6.2-7.37,0-15.32,1.45-21.88,4.94s-11.85,9.19-11.85,16.9,5.42,13.48,11.85,16.9,14.51,4.94,21.88,4.94a54.52,54.52,0,0,0,9.29-.78l77.41,20.93L256,229.42,83,142.17,256,58Z"/><path class="cls-2" d="M256,130.18c8.43,0,15.9,2.18,20.52,5.55l140,37.84v72.75a29.68,29.68,0,0,0-18.51,0V192l-132.36-35.8a45,45,0,0,1-9.6,1c-14,0-25.42-6.05-25.42-13.53S242,130.18,256,130.18Z"/><circle class="cls-3" cx="407.23" cy="274.28" r="22.68"/></svg>
@@ -0,0 +1,85 @@
1
+ "use server";
2
+
3
+ import { headers } from "next/headers";
4
+ import { redirect } from "next/navigation";
5
+ import { z } from "zod";
6
+ import { getClientIp, safeRedirectPath } from "@clubedge/core";
7
+ import { clientEnv } from "@/env/client";
8
+ import { loginUrl } from "@/lib/login-url";
9
+ import { getAuth } from "@/server/auth";
10
+ import { createRateLimiter } from "@/server/cache";
11
+ import { ensureUserProfile } from "@/server/users";
12
+
13
+ const credentialsSchema = z.object({
14
+ email: z.email(),
15
+ password: z.string().min(8).max(128),
16
+ });
17
+
18
+ // Ten attempts per minute per client address and action.
19
+ const authRateLimiter = createRateLimiter(10, 60);
20
+
21
+ function readCredentials(formData: FormData) {
22
+ return credentialsSchema.safeParse({
23
+ email: formData.get("email"),
24
+ password: formData.get("password"),
25
+ });
26
+ }
27
+
28
+ async function isRateLimited(action: "sign-in" | "sign-up") {
29
+ const ip = getClientIp(await headers());
30
+ try {
31
+ const result = await authRateLimiter.limit(`auth:${action}:${ip}`);
32
+ return !result.success;
33
+ } catch (error) {
34
+ // Fail open: an unavailable limiter backend should not lock every user out.
35
+ console.error("Rate limiter unavailable", error);
36
+ return false;
37
+ }
38
+ }
39
+
40
+ export async function signIn(formData: FormData) {
41
+ const next = safeRedirectPath(formData.get("next"));
42
+ if (await isRateLimited("sign-in")) redirect(loginUrl({ error: "rate-limited", next }));
43
+
44
+ const parsed = readCredentials(formData);
45
+ if (!parsed.success) redirect(loginUrl({ error: "invalid-input", next }));
46
+
47
+ const auth = await getAuth();
48
+ const result = await auth.signInWithPassword(parsed.data);
49
+ if (!result.ok) redirect(loginUrl({ error: "invalid-credentials", next }));
50
+
51
+ await ensureUserProfile(result.value);
52
+ redirect(next);
53
+ }
54
+
55
+ export async function signUp(formData: FormData) {
56
+ const next = safeRedirectPath(formData.get("next"));
57
+ if (await isRateLimited("sign-up")) {
58
+ redirect(loginUrl({ mode: "signup", error: "rate-limited", next }));
59
+ }
60
+
61
+ const parsed = readCredentials(formData);
62
+ if (!parsed.success) redirect(loginUrl({ mode: "signup", error: "invalid-input", next }));
63
+
64
+ const callbackUrl = new URL("/auth/callback", clientEnv.NEXT_PUBLIC_APP_URL);
65
+ callbackUrl.searchParams.set("next", next);
66
+ const auth = await getAuth();
67
+ const result = await auth.signUp({ ...parsed.data, emailRedirectTo: callbackUrl.toString() });
68
+ if (!result.ok) redirect(loginUrl({ mode: "signup", error: "signup-failed", next }));
69
+ if (result.value.needsConfirmation || !result.value.user) {
70
+ redirect(loginUrl({ mode: "signup", checkEmail: true, next }));
71
+ }
72
+
73
+ await ensureUserProfile(result.value.user);
74
+ redirect(next);
75
+ }
76
+
77
+ export async function signOut() {
78
+ try {
79
+ const auth = await getAuth();
80
+ await auth.signOut();
81
+ } catch (error) {
82
+ console.error("Sign-out failed", error);
83
+ }
84
+ redirect("/login");
85
+ }
@@ -0,0 +1,11 @@
1
+ import { siteConfig } from "@/config/site";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ export function GET() {
6
+ return Response.json({
7
+ status: "ok",
8
+ service: siteConfig.serviceId,
9
+ timestamp: new Date().toISOString(),
10
+ });
11
+ }
@@ -0,0 +1,21 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+ import { safeRedirectPath } from "@clubedge/core";
3
+ import { loginUrl } from "@/lib/login-url";
4
+ import { getAuth } from "@/server/auth";
5
+ import { ensureUserProfile } from "@/server/users";
6
+
7
+ export async function GET(request: NextRequest) {
8
+ const code = request.nextUrl.searchParams.get("code");
9
+ const next = safeRedirectPath(request.nextUrl.searchParams.get("next"));
10
+
11
+ if (code) {
12
+ const auth = await getAuth();
13
+ const result = await auth.exchangeCodeForSession(code);
14
+ if (result.ok) {
15
+ await ensureUserProfile(result.value);
16
+ return NextResponse.redirect(new URL(next, request.url));
17
+ }
18
+ }
19
+
20
+ return NextResponse.redirect(new URL(loginUrl({ error: "auth-callback" }), request.url));
21
+ }
@@ -0,0 +1,34 @@
1
+ import Link from "next/link";
2
+
3
+ import { signOut } from "@/app/actions/auth";
4
+ import type { AuthUser } from "@clubedge/auth";
5
+ import { Button } from "@clubedge/ui/components/button";
6
+
7
+ type AccountButtonProps = {
8
+ user: AuthUser | null;
9
+ className?: string;
10
+ };
11
+
12
+ export function AccountButton({ user, className }: AccountButtonProps) {
13
+ if (!user) {
14
+ return (
15
+ <Button className={className} render={<Link href="/login" />} size="sm" variant="outline">
16
+ Sign in
17
+ </Button>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <form action={signOut} className={className}>
23
+ <Button
24
+ className="w-full"
25
+ size="sm"
26
+ title={user.email ?? undefined}
27
+ type="submit"
28
+ variant="outline"
29
+ >
30
+ Sign out
31
+ </Button>
32
+ </form>
33
+ );
34
+ }
@@ -0,0 +1,91 @@
1
+ import { Activity, ArrowUpRight, Blocks, Github, LayoutDashboard } from "lucide-react";
2
+ import Link from "next/link";
3
+
4
+ import { ClubedgeMark } from "@/components/branding/clubedge-mark";
5
+ import { siteConfig } from "@/config/site";
6
+ import type { AuthUser } from "@clubedge/auth";
7
+ import {
8
+ Sidebar,
9
+ SidebarContent,
10
+ SidebarFooter,
11
+ SidebarGroup,
12
+ SidebarGroupLabel,
13
+ SidebarHeader,
14
+ SidebarMenu,
15
+ SidebarMenuButton,
16
+ SidebarMenuItem,
17
+ SidebarSeparator,
18
+ } from "@clubedge/ui/components/sidebar";
19
+
20
+ import { AccountButton } from "./account-button";
21
+
22
+ const navigation = [
23
+ { href: "#overview", label: "Overview", icon: LayoutDashboard },
24
+ { href: "#modules", label: "Modules", icon: Blocks },
25
+ { href: "#activity", label: "Setup status", icon: Activity },
26
+ ];
27
+
28
+ export function AppSidebar({ user }: { user: AuthUser | null }) {
29
+ return (
30
+ <Sidebar collapsible="icon" variant="inset">
31
+ <SidebarHeader>
32
+ <SidebarMenu>
33
+ <SidebarMenuItem>
34
+ <SidebarMenuButton
35
+ className="h-12"
36
+ render={<Link href="#overview" aria-label={`${siteConfig.name} home`} />}
37
+ size="lg"
38
+ tooltip={siteConfig.name}
39
+ >
40
+ <ClubedgeMark alt="" className="size-8 shrink-0 rounded-lg" />
41
+ <span className="grid min-w-0 text-start leading-tight">
42
+ <span className="truncate font-semibold">{siteConfig.shortName}</span>
43
+ <span className="truncate text-xs text-sidebar-foreground/70">
44
+ {siteConfig.workspaceLabel}
45
+ </span>
46
+ </span>
47
+ </SidebarMenuButton>
48
+ </SidebarMenuItem>
49
+ </SidebarMenu>
50
+ </SidebarHeader>
51
+
52
+ <SidebarSeparator />
53
+
54
+ <SidebarContent>
55
+ <SidebarGroup>
56
+ <SidebarGroupLabel>Workspace</SidebarGroupLabel>
57
+ <SidebarMenu>
58
+ {navigation.map(({ href, label, icon: Icon }, index) => (
59
+ <SidebarMenuItem key={href}>
60
+ <SidebarMenuButton
61
+ render={<Link href={href} aria-current={index === 0 ? "page" : undefined} />}
62
+ isActive={index === 0}
63
+ tooltip={label}
64
+ >
65
+ <Icon aria-hidden="true" />
66
+ <span>{label}</span>
67
+ </SidebarMenuButton>
68
+ </SidebarMenuItem>
69
+ ))}
70
+ </SidebarMenu>
71
+ </SidebarGroup>
72
+ </SidebarContent>
73
+
74
+ <SidebarFooter>
75
+ <SidebarMenu>
76
+ <SidebarMenuItem>
77
+ <SidebarMenuButton
78
+ render={<Link href={siteConfig.links.repository} target="_blank" rel="noreferrer" />}
79
+ tooltip="Starter repository"
80
+ >
81
+ <Github aria-hidden="true" />
82
+ <span>Starter repository</span>
83
+ <ArrowUpRight aria-hidden="true" className="ms-auto" />
84
+ </SidebarMenuButton>
85
+ </SidebarMenuItem>
86
+ </SidebarMenu>
87
+ <AccountButton className="w-full" user={user} />
88
+ </SidebarFooter>
89
+ </Sidebar>
90
+ );
91
+ }