kofi-stack-template-generator 2.1.16 → 2.1.18
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/dist/index.js +73 -26
- package/package.json +1 -1
- package/src/templates.generated.ts +5 -5
- package/templates/convex/convex/users.ts.hbs +12 -2
- package/templates/marketing/nextjs/src/app/page.tsx.hbs +41 -0
- package/templates/web/src/components/dashboard/app-sidebar.tsx.hbs +18 -14
- package/templates/web/src/components/dashboard/dashboard-layout.tsx.hbs +13 -11
package/dist/index.js
CHANGED
|
@@ -249,7 +249,7 @@ var EMBEDDED_TEMPLATES = {
|
|
|
249
249
|
"convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
|
|
250
250
|
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { authComponent, createAuth } from './auth'\n\nconst http = httpRouter()\n\n// Register Better Auth routes\nauthComponent.registerRoutes(http, createAuth)\n\nexport default http\n",
|
|
251
251
|
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { v } from 'convex/values'\n\n// Better Auth manages its own tables via the betterAuth component\n// Add your custom application tables here\nexport default defineSchema({\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // userId: v.string(), // Better Auth user ID\n // createdAt: v.number(),\n // }).index('by_user', ['userId']),\n})\n",
|
|
252
|
-
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { authComponent } from './auth'\n\n// Get current user from Better Auth session\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n",
|
|
252
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { authComponent } from './auth'\n\n// Get current user from Better Auth session\n// Returns null if not authenticated (instead of throwing)\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n try {\n return await authComponent.getAuthUser(ctx)\n } catch {\n return null\n }\n },\n})\n\n// Alias for current user - used by dashboard components\n// Returns null if not authenticated (instead of throwing)\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n try {\n return await authComponent.getAuthUser(ctx)\n } catch {\n return null\n }\n },\n})\n",
|
|
253
253
|
"convex/package.json.hbs": `{{#if (eq structure 'monorepo')}}{
|
|
254
254
|
"name": "@repo/backend",
|
|
255
255
|
"version": "0.1.0",
|
|
@@ -309,6 +309,47 @@ var EMBEDDED_TEMPLATES = {
|
|
|
309
309
|
"marketing/nextjs/src/app/page.tsx.hbs": `export default function HomePage() {
|
|
310
310
|
return (
|
|
311
311
|
<main className="min-h-screen">
|
|
312
|
+
{/* Navigation */}
|
|
313
|
+
<nav className="sticky top-0 z-50 w-full border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
|
|
314
|
+
<div className="container mx-auto px-4">
|
|
315
|
+
<div className="flex h-16 items-center justify-between">
|
|
316
|
+
<div className="flex items-center gap-2">
|
|
317
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 dark:bg-white">
|
|
318
|
+
<svg className="h-4 w-4 text-white dark:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
319
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
320
|
+
</svg>
|
|
321
|
+
</div>
|
|
322
|
+
<span className="font-semibold text-gray-900 dark:text-white">{{projectName}}</span>
|
|
323
|
+
</div>
|
|
324
|
+
<div className="hidden md:flex items-center gap-6">
|
|
325
|
+
<a href="#features" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
326
|
+
Features
|
|
327
|
+
</a>
|
|
328
|
+
<a href="#" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
329
|
+
Pricing
|
|
330
|
+
</a>
|
|
331
|
+
<a href="#" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
332
|
+
Docs
|
|
333
|
+
</a>
|
|
334
|
+
</div>
|
|
335
|
+
<div className="flex items-center gap-4">
|
|
336
|
+
<a
|
|
337
|
+
href="/sign-in"
|
|
338
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
|
|
339
|
+
>
|
|
340
|
+
Sign in
|
|
341
|
+
</a>
|
|
342
|
+
<a
|
|
343
|
+
href="/sign-up"
|
|
344
|
+
className="rounded-lg bg-gray-900 dark:bg-white px-4 py-2 text-sm font-medium text-white dark:text-gray-900 hover:opacity-90 transition-opacity"
|
|
345
|
+
>
|
|
346
|
+
Get Started
|
|
347
|
+
</a>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</nav>
|
|
352
|
+
|
|
312
353
|
{/* Hero Section */}
|
|
313
354
|
<section className="relative overflow-hidden bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
|
|
314
355
|
<div className="container mx-auto px-4 py-24 sm:py-32">
|
|
@@ -1090,6 +1131,8 @@ import {
|
|
|
1090
1131
|
SidebarMenu,
|
|
1091
1132
|
SidebarMenuButton,
|
|
1092
1133
|
SidebarMenuItem,
|
|
1134
|
+
SidebarRail,
|
|
1135
|
+
useSidebar,
|
|
1093
1136
|
} from '@/components/ui/sidebar'
|
|
1094
1137
|
import {
|
|
1095
1138
|
DropdownMenu,
|
|
@@ -1116,6 +1159,7 @@ const navigation = [
|
|
|
1116
1159
|
export function AppSidebar() {
|
|
1117
1160
|
const router = useRouter()
|
|
1118
1161
|
const { data: session } = useSession()
|
|
1162
|
+
const { isMobile } = useSidebar()
|
|
1119
1163
|
const user = session?.user
|
|
1120
1164
|
|
|
1121
1165
|
const handleSignOut = async () => {
|
|
@@ -1134,18 +1178,18 @@ export function AppSidebar() {
|
|
|
1134
1178
|
}
|
|
1135
1179
|
|
|
1136
1180
|
return (
|
|
1137
|
-
<Sidebar>
|
|
1181
|
+
<Sidebar collapsible="icon">
|
|
1138
1182
|
<SidebarHeader>
|
|
1139
1183
|
<SidebarMenu>
|
|
1140
1184
|
<SidebarMenuItem>
|
|
1141
|
-
<SidebarMenuButton size="lg" asChild>
|
|
1185
|
+
<SidebarMenuButton size="lg" asChild tooltip="{{projectName}}">
|
|
1142
1186
|
<a href="/">
|
|
1143
|
-
<div className="flex aspect-square size-8 items-center justify-center rounded-lg
|
|
1187
|
+
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
1144
1188
|
<GalleryVerticalEnd className="size-4" />
|
|
1145
1189
|
</div>
|
|
1146
|
-
<div className="
|
|
1147
|
-
<span className="font-semibold">{{projectName}}</span>
|
|
1148
|
-
<span className="text-xs
|
|
1190
|
+
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
|
1191
|
+
<span className="truncate font-semibold">{{projectName}}</span>
|
|
1192
|
+
<span className="truncate text-xs">Dashboard</span>
|
|
1149
1193
|
</div>
|
|
1150
1194
|
</a>
|
|
1151
1195
|
</SidebarMenuButton>
|
|
@@ -1159,9 +1203,9 @@ export function AppSidebar() {
|
|
|
1159
1203
|
<SidebarMenu>
|
|
1160
1204
|
{navigation.map((item) => (
|
|
1161
1205
|
<SidebarMenuItem key={item.title}>
|
|
1162
|
-
<SidebarMenuButton asChild>
|
|
1206
|
+
<SidebarMenuButton asChild tooltip={item.title}>
|
|
1163
1207
|
<a href={item.url}>
|
|
1164
|
-
<item.icon
|
|
1208
|
+
<item.icon />
|
|
1165
1209
|
<span>{item.title}</span>
|
|
1166
1210
|
</a>
|
|
1167
1211
|
</SidebarMenuButton>
|
|
@@ -1188,7 +1232,7 @@ export function AppSidebar() {
|
|
|
1188
1232
|
</Avatar>
|
|
1189
1233
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
1190
1234
|
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
|
|
1191
|
-
<span className="truncate text-xs
|
|
1235
|
+
<span className="truncate text-xs">
|
|
1192
1236
|
{user?.email ?? ''}
|
|
1193
1237
|
</span>
|
|
1194
1238
|
</div>
|
|
@@ -1196,26 +1240,26 @@ export function AppSidebar() {
|
|
|
1196
1240
|
</SidebarMenuButton>
|
|
1197
1241
|
</DropdownMenuTrigger>
|
|
1198
1242
|
<DropdownMenuContent
|
|
1199
|
-
className="w-
|
|
1200
|
-
side=
|
|
1243
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
1244
|
+
side={isMobile ? 'bottom' : 'right'}
|
|
1201
1245
|
align="end"
|
|
1202
1246
|
sideOffset={4}
|
|
1203
1247
|
>
|
|
1204
1248
|
<DropdownMenuItem asChild>
|
|
1205
1249
|
<a href="/settings">
|
|
1206
|
-
<User
|
|
1250
|
+
<User />
|
|
1207
1251
|
Profile
|
|
1208
1252
|
</a>
|
|
1209
1253
|
</DropdownMenuItem>
|
|
1210
1254
|
<DropdownMenuItem asChild>
|
|
1211
1255
|
<a href="/settings">
|
|
1212
|
-
<Settings
|
|
1256
|
+
<Settings />
|
|
1213
1257
|
Settings
|
|
1214
1258
|
</a>
|
|
1215
1259
|
</DropdownMenuItem>
|
|
1216
1260
|
<DropdownMenuSeparator />
|
|
1217
1261
|
<DropdownMenuItem onClick={handleSignOut}>
|
|
1218
|
-
<LogOut
|
|
1262
|
+
<LogOut />
|
|
1219
1263
|
Sign out
|
|
1220
1264
|
</DropdownMenuItem>
|
|
1221
1265
|
</DropdownMenuContent>
|
|
@@ -1223,6 +1267,7 @@ export function AppSidebar() {
|
|
|
1223
1267
|
</SidebarMenuItem>
|
|
1224
1268
|
</SidebarMenu>
|
|
1225
1269
|
</SidebarFooter>
|
|
1270
|
+
<SidebarRail />
|
|
1226
1271
|
</Sidebar>
|
|
1227
1272
|
)
|
|
1228
1273
|
}
|
|
@@ -1249,18 +1294,20 @@ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayo
|
|
|
1249
1294
|
<SidebarProvider>
|
|
1250
1295
|
<AppSidebar />
|
|
1251
1296
|
<SidebarInset>
|
|
1252
|
-
<header className="flex h-16 shrink-0 items-center gap-2
|
|
1253
|
-
<
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
<
|
|
1257
|
-
<
|
|
1258
|
-
<
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1297
|
+
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
1298
|
+
<div className="flex items-center gap-2 px-4">
|
|
1299
|
+
<SidebarTrigger className="-ml-1" />
|
|
1300
|
+
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
|
1301
|
+
<Breadcrumb>
|
|
1302
|
+
<BreadcrumbList>
|
|
1303
|
+
<BreadcrumbItem>
|
|
1304
|
+
<BreadcrumbPage>{title}</BreadcrumbPage>
|
|
1305
|
+
</BreadcrumbItem>
|
|
1306
|
+
</BreadcrumbList>
|
|
1307
|
+
</Breadcrumb>
|
|
1308
|
+
</div>
|
|
1262
1309
|
</header>
|
|
1263
|
-
<main className="flex-1
|
|
1310
|
+
<main className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</main>
|
|
1264
1311
|
</SidebarInset>
|
|
1265
1312
|
</SidebarProvider>
|
|
1266
1313
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Auto-generated file. Do not edit manually.
|
|
2
2
|
// Run 'pnpm prebuild' to regenerate.
|
|
3
|
-
// Generated: 2026-01-15T02:
|
|
3
|
+
// Generated: 2026-01-15T02:23:02.427Z
|
|
4
4
|
// Template count: 90
|
|
5
5
|
|
|
6
6
|
export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
@@ -12,7 +12,7 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
12
12
|
"convex/convex/convex.config.ts.hbs": "import { defineApp } from 'convex/server'\nimport betterAuth from '@convex-dev/better-auth/convex.config'\n\nconst app = defineApp()\napp.use(betterAuth)\n\nexport default app\n",
|
|
13
13
|
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { authComponent, createAuth } from './auth'\n\nconst http = httpRouter()\n\n// Register Better Auth routes\nauthComponent.registerRoutes(http, createAuth)\n\nexport default http\n",
|
|
14
14
|
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { v } from 'convex/values'\n\n// Better Auth manages its own tables via the betterAuth component\n// Add your custom application tables here\nexport default defineSchema({\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // userId: v.string(), // Better Auth user ID\n // createdAt: v.number(),\n // }).index('by_user', ['userId']),\n})\n",
|
|
15
|
-
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { authComponent } from './auth'\n\n// Get current user from Better Auth session\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n return authComponent.getAuthUser(ctx)\n },\n})\n",
|
|
15
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { authComponent } from './auth'\n\n// Get current user from Better Auth session\n// Returns null if not authenticated (instead of throwing)\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n try {\n return await authComponent.getAuthUser(ctx)\n } catch {\n return null\n }\n },\n})\n\n// Alias for current user - used by dashboard components\n// Returns null if not authenticated (instead of throwing)\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n try {\n return await authComponent.getAuthUser(ctx)\n } catch {\n return null\n }\n },\n})\n",
|
|
16
16
|
"convex/package.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"name\": \"@repo/backend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./convex/_generated/api.js\",\n \"types\": \"./convex/_generated/api.d.ts\"\n }\n },\n \"scripts\": {\n \"dev\": \"convex dev\",\n \"dev:setup\": \"convex dev --configure --until-success\",\n \"deploy\": \"convex deploy\"\n },\n \"dependencies\": {\n \"convex\": \"^1.25.0\",\n \"@convex-dev/better-auth\": \"^0.10.0\",\n \"better-auth\": \"1.4.9\",\n \"@convex-dev/resend\": \"^0.2.0\"{{#if (eq integrations.uploads 'convex-fs')}},\n \"convex-fs\": \"^0.2.0\"{{/if}}{{#if (eq integrations.uploads 'r2')}},\n \"@convex-dev/r2\": \"^0.8.0\"{{/if}}{{#if (eq integrations.payments 'stripe')}},\n \"@convex-dev/stripe\": \"^0.1.0\"{{/if}}{{#if (eq integrations.payments 'polar')}},\n \"@convex-dev/polar\": \"^0.7.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@convex-dev/rate-limiter\": \"^0.3.0\"{{/if}}\n },\n \"devDependencies\": {\n \"@types/node\": \"^20.0.0\",\n \"typescript\": \"^5.0.0\"\n }\n}{{/if}}\n",
|
|
17
17
|
"convex/tsconfig.json.hbs": "{{#if (eq structure 'monorepo')}}{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true,\n \"outDir\": \"dist\"\n },\n \"include\": [\"convex/**/*.ts\"],\n \"exclude\": [\"node_modules\"]\n}{{/if}}\n",
|
|
18
18
|
"integrations/posthog/src/components/providers/posthog-provider.tsx.hbs": "'use client'\n\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PHProvider } from 'posthog-js/react'\nimport { useEffect } from 'react'\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',\n person_profiles: 'identified_only',\n capture_pageview: false, // We capture pageviews manually\n })\n }, [])\n\n return <PHProvider client={posthog}>{children}</PHProvider>\n}\n",
|
|
@@ -21,7 +21,7 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
21
21
|
"marketing/nextjs/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
22
22
|
"marketing/nextjs/src/app/globals.css.hbs": "@import \"tailwindcss\";\n\n:root {\n --background: #ffffff;\n --foreground: #171717;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --background: #0a0a0a;\n --foreground: #ededed;\n }\n}\n\nbody {\n color: var(--foreground);\n background: var(--background);\n}\n",
|
|
23
23
|
"marketing/nextjs/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}} - Marketing',\n description: 'Marketing site for {{projectName}}',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <html lang=\"en\">\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n {children}\n </body>\n </html>\n )\n}\n",
|
|
24
|
-
"marketing/nextjs/src/app/page.tsx.hbs": "export default function HomePage() {\n return (\n <main className=\"min-h-screen\">\n {/* Hero Section */}\n <section className=\"relative overflow-hidden bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800\">\n <div className=\"container mx-auto px-4 py-24 sm:py-32\">\n <div className=\"text-center\">\n <h1 className=\"text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl\">\n Welcome to {{projectName}}\n </h1>\n <p className=\"mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto\">\n A modern full-stack application built with Next.js, Convex, and Better-Auth.\n </p>\n <div className=\"mt-10 flex items-center justify-center gap-x-6\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-gray-900 dark:bg-white px-6 py-3 text-sm font-semibold text-white dark:text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Get Started\n </a>\n <a\n href=\"#features\"\n className=\"text-sm font-semibold leading-6 text-gray-900 dark:text-white\"\n >\n Learn more <span aria-hidden=\"true\">→</span>\n </a>\n </div>\n </div>\n </div>\n </section>\n\n {/* Features Section */}\n <section id=\"features\" className=\"py-24 sm:py-32\">\n <div className=\"container mx-auto px-4\">\n <div className=\"text-center mb-16\">\n <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl\">\n Everything you need\n </h2>\n <p className=\"mt-4 text-lg text-gray-600 dark:text-gray-300\">\n Built with the best tools for modern web development\n </p>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Lightning Fast\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Built on Next.js with Turbopack for instant hot reload and optimized production builds.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Real-time Database\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Powered by Convex for automatic real-time sync and type-safe queries.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Secure Auth\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Better-Auth provides secure, flexible authentication with social login support.\n </p>\n </div>\n </div>\n </div>\n </section>\n\n {/* CTA Section */}\n <section className=\"py-24 sm:py-32 bg-gray-900 dark:bg-gray-800\">\n <div className=\"container mx-auto px-4 text-center\">\n <h2 className=\"text-3xl font-bold tracking-tight text-white sm:text-4xl\">\n Ready to get started?\n </h2>\n <p className=\"mt-4 text-lg text-gray-300 max-w-2xl mx-auto\">\n Start building your next project with our full-stack template.\n </p>\n <div className=\"mt-10\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-white px-6 py-3 text-sm font-semibold text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Launch App\n </a>\n </div>\n </div>\n </section>\n\n {/* Footer */}\n <footer className=\"border-t border-gray-200 dark:border-gray-700\">\n <div className=\"container mx-auto px-4 py-12\">\n <div className=\"text-center text-gray-600 dark:text-gray-300\">\n <p>\n Built with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-gray-900 dark:text-white hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </footer>\n </main>\n )\n}\n",
|
|
24
|
+
"marketing/nextjs/src/app/page.tsx.hbs": "export default function HomePage() {\n return (\n <main className=\"min-h-screen\">\n {/* Navigation */}\n <nav className=\"sticky top-0 z-50 w-full border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm\">\n <div className=\"container mx-auto px-4\">\n <div className=\"flex h-16 items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 dark:bg-white\">\n <svg className=\"h-4 w-4 text-white dark:text-gray-900\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n </div>\n <span className=\"font-semibold text-gray-900 dark:text-white\">{{projectName}}</span>\n </div>\n <div className=\"hidden md:flex items-center gap-6\">\n <a href=\"#features\" className=\"text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors\">\n Features\n </a>\n <a href=\"#\" className=\"text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors\">\n Pricing\n </a>\n <a href=\"#\" className=\"text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors\">\n Docs\n </a>\n </div>\n <div className=\"flex items-center gap-4\">\n <a\n href=\"/sign-in\"\n className=\"text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors\"\n >\n Sign in\n </a>\n <a\n href=\"/sign-up\"\n className=\"rounded-lg bg-gray-900 dark:bg-white px-4 py-2 text-sm font-medium text-white dark:text-gray-900 hover:opacity-90 transition-opacity\"\n >\n Get Started\n </a>\n </div>\n </div>\n </div>\n </nav>\n\n {/* Hero Section */}\n <section className=\"relative overflow-hidden bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800\">\n <div className=\"container mx-auto px-4 py-24 sm:py-32\">\n <div className=\"text-center\">\n <h1 className=\"text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl\">\n Welcome to {{projectName}}\n </h1>\n <p className=\"mt-6 text-lg leading-8 text-gray-600 dark:text-gray-300 max-w-2xl mx-auto\">\n A modern full-stack application built with Next.js, Convex, and Better-Auth.\n </p>\n <div className=\"mt-10 flex items-center justify-center gap-x-6\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-gray-900 dark:bg-white px-6 py-3 text-sm font-semibold text-white dark:text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Get Started\n </a>\n <a\n href=\"#features\"\n className=\"text-sm font-semibold leading-6 text-gray-900 dark:text-white\"\n >\n Learn more <span aria-hidden=\"true\">→</span>\n </a>\n </div>\n </div>\n </div>\n </section>\n\n {/* Features Section */}\n <section id=\"features\" className=\"py-24 sm:py-32\">\n <div className=\"container mx-auto px-4\">\n <div className=\"text-center mb-16\">\n <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl\">\n Everything you need\n </h2>\n <p className=\"mt-4 text-lg text-gray-600 dark:text-gray-300\">\n Built with the best tools for modern web development\n </p>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Lightning Fast\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Built on Next.js with Turbopack for instant hot reload and optimized production builds.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Real-time Database\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Powered by Convex for automatic real-time sync and type-safe queries.\n </p>\n </div>\n <div className=\"p-6 rounded-xl border border-gray-200 dark:border-gray-700\">\n <div className=\"w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center mb-4\">\n <svg className=\"w-6 h-6 text-gray-900 dark:text-white\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\" />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white mb-2\">\n Secure Auth\n </h3>\n <p className=\"text-gray-600 dark:text-gray-300\">\n Better-Auth provides secure, flexible authentication with social login support.\n </p>\n </div>\n </div>\n </div>\n </section>\n\n {/* CTA Section */}\n <section className=\"py-24 sm:py-32 bg-gray-900 dark:bg-gray-800\">\n <div className=\"container mx-auto px-4 text-center\">\n <h2 className=\"text-3xl font-bold tracking-tight text-white sm:text-4xl\">\n Ready to get started?\n </h2>\n <p className=\"mt-4 text-lg text-gray-300 max-w-2xl mx-auto\">\n Start building your next project with our full-stack template.\n </p>\n <div className=\"mt-10\">\n <a\n href=\"/app\"\n className=\"rounded-lg bg-white px-6 py-3 text-sm font-semibold text-gray-900 shadow-sm hover:opacity-90 transition-opacity\"\n >\n Launch App\n </a>\n </div>\n </div>\n </section>\n\n {/* Footer */}\n <footer className=\"border-t border-gray-200 dark:border-gray-700\">\n <div className=\"container mx-auto px-4 py-12\">\n <div className=\"text-center text-gray-600 dark:text-gray-300\">\n <p>\n Built with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-gray-900 dark:text-white hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </footer>\n </main>\n )\n}\n",
|
|
25
25
|
"marketing/nextjs/tsconfig.json.hbs": "{\n \"compilerOptions\": {\n \"target\": \"ES2017\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"incremental\": true,\n \"plugins\": [{ \"name\": \"next\" }],\n \"paths\": {\n \"@/*\": [\"./src/*\"]\n }\n },\n \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \".next/dev/types/**/*.ts\"],\n \"exclude\": [\"node_modules\"]\n}\n",
|
|
26
26
|
"marketing/payload/_env.example.hbs": "# Database (Supabase PostgreSQL)\nDATABASE_URL=\"postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres\"\n\n# Payload CMS\nPAYLOAD_SECRET=\"\" # Generate with: openssl rand -base64 32\n\n# Scheduled Jobs\nCRON_SECRET=\"\" # Generate with: openssl rand -base64 32\n\n# Draft Previews\nPREVIEW_SECRET=\"\" # Generate with: openssl rand -base64 32\n\n# S3 Storage (Supabase Storage)\nS3_BUCKET=\"media\"\nS3_ACCESS_KEY_ID=\"\"\nS3_SECRET_ACCESS_KEY=\"\"\nS3_REGION=\"auto\"\nS3_ENDPOINT=\"https://[PROJECT].supabase.co/storage/v1/s3\"\n",
|
|
27
27
|
"marketing/payload/_env.local.hbs": "# Database (Supabase PostgreSQL)\nDATABASE_URL=\n\n# Payload CMS\nPAYLOAD_SECRET=\n\n# Scheduled Jobs\nCRON_SECRET=\n\n# Draft Previews\nPREVIEW_SECRET=\n\n# S3 Storage (Supabase Storage)\nS3_BUCKET=\"media\"\nS3_ACCESS_KEY_ID=\nS3_SECRET_ACCESS_KEY=\nS3_REGION=\"auto\"\nS3_ENDPOINT=\n",
|
|
@@ -86,8 +86,8 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
|
86
86
|
"web/src/app/page.tsx.hbs": "'use client'\n\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../convex/_generated/api{{/if}}'\nimport { DashboardLayout } from '@/components/dashboard/dashboard-layout'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Skeleton } from '@/components/ui/skeleton'\n\nexport default function HomePage() {\n const user = useQuery(api.users.viewer)\n\n if (user === undefined) {\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <Skeleton className=\"h-8 w-64\" />\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n </DashboardLayout>\n )\n }\n\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">\n Welcome back{user?.name ? `, ${user.name}` : ''}!\n </h1>\n <p className=\"text-muted-foreground\">\n Here's what's happening with your project today.\n </p>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Card>\n <CardHeader>\n <CardTitle>Getting Started</CardTitle>\n <CardDescription>Quick start guide for your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Customize your dashboard layout</li>\n <li>Add new pages to the sidebar</li>\n <li>Connect your data sources</li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Documentation</CardTitle>\n <CardDescription>Learn how to build with Kofi Stack</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>\n <a\n href=\"https://docs.convex.dev\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Convex Documentation\n </a>\n </li>\n <li>\n <a\n href=\"https://ui.shadcn.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n shadcn/ui Components\n </a>\n </li>\n <li>\n <a\n href=\"https://nextjs.org/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Next.js Documentation\n </a>\n </li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Your Stack</CardTitle>\n <CardDescription>Technologies powering your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Next.js 15 with App Router</li>\n <li>Convex for backend & database</li>\n <li>Convex Auth for authentication</li>\n <li>shadcn/ui components</li>\n <li>Tailwind CSS for styling</li>\n </ul>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </DashboardLayout>\n )\n}\n",
|
|
87
87
|
"web/src/components/auth/sign-in-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignInForm() {\n const router = useRouter()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const result = await signIn.email({\n email,\n password,\n })\n\n if (result.error) {\n setError(result.error.message || 'Invalid email or password')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Invalid email or password')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Welcome back</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"Enter your password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Signing in...' : 'Sign In'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Don't have an account?{' '}\n <Link href=\"/sign-up\" className=\"text-primary hover:underline font-medium\">\n Sign up\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
|
|
88
88
|
"web/src/components/auth/sign-up-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signUp, signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignUpForm() {\n const router = useRouter()\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n if (password.length < 8) {\n setError('Password must be at least 8 characters')\n setIsLoading(false)\n return\n }\n\n try {\n const result = await signUp.email({\n email,\n password,\n name,\n })\n\n if (result.error) {\n setError(result.error.message || 'Failed to create account')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Failed to create account. Email may already be in use.')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Create an account</CardTitle>\n <CardDescription>Enter your details to get started</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">Name</Label>\n <Input\n id=\"name\"\n type=\"text\"\n placeholder=\"Your name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"At least 8 characters\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n minLength={8}\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Creating account...' : 'Create Account'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Already have an account?{' '}\n <Link href=\"/sign-in\" className=\"text-primary hover:underline font-medium\">\n Sign in\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
|
|
89
|
-
"web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar>\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild>\n <a href=\"/\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg
|
|
90
|
-
"web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2
|
|
89
|
+
"web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarRail,\n useSidebar,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const { isMobile } = useSidebar()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar collapsible=\"icon\">\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild tooltip=\"{{projectName}}\">\n <a href=\"/\">\n <div className=\"bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden\">\n <span className=\"truncate font-semibold\">{{projectName}}</span>\n <span className=\"truncate text-xs\">Dashboard</span>\n </div>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {navigation.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild tooltip={item.title}>\n <a href={item.url}>\n <item.icon />\n <span>{item.title}</span>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarFooter>\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />\n <AvatarFallback className=\"rounded-lg\">\n {getInitials(user?.name)}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{user?.name ?? 'User'}</span>\n <span className=\"truncate text-xs\">\n {user?.email ?? ''}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n side={isMobile ? 'bottom' : 'right'}\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <User />\n Profile\n </a>\n </DropdownMenuItem>\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <Settings />\n Settings\n </a>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleSignOut}>\n <LogOut />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n <SidebarRail />\n </Sidebar>\n )\n}\n",
|
|
90
|
+
"web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12\">\n <div className=\"flex items-center gap-2 px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 data-[orientation=vertical]:h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbPage>{title}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </div>\n </header>\n <main className=\"flex flex-1 flex-col gap-4 p-4 pt-0\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n",
|
|
91
91
|
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexReactClient } from 'convex/react'\nimport { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'\nimport { authClient } from '@/lib/auth'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexBetterAuthProvider client={convex} authClient={authClient}>\n {children}\n </ConvexBetterAuthProvider>\n )\n}\n",
|
|
92
92
|
"web/src/lib/auth-server.ts.hbs": "import { convexBetterAuthNextJs } from '@convex-dev/better-auth/nextjs'\n\nexport const {\n handler,\n preloadAuthQuery,\n isAuthenticated,\n getToken,\n fetchAuthQuery,\n fetchAuthMutation,\n fetchAuthAction,\n} = convexBetterAuthNextJs({\n convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,\n convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL || process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',\n})\n",
|
|
93
93
|
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { createAuthClient } from 'better-auth/react'\nimport { convexClient } from '@convex-dev/better-auth/client/plugins'\n\nexport const authClient = createAuthClient({\n plugins: [convexClient()],\n})\n\nexport const {\n signIn,\n signUp,\n signOut,\n useSession,\n getSession,\n} = authClient\n",
|
|
@@ -2,17 +2,27 @@ import { query } from './_generated/server'
|
|
|
2
2
|
import { authComponent } from './auth'
|
|
3
3
|
|
|
4
4
|
// Get current user from Better Auth session
|
|
5
|
+
// Returns null if not authenticated (instead of throwing)
|
|
5
6
|
export const current = query({
|
|
6
7
|
args: {},
|
|
7
8
|
handler: async (ctx) => {
|
|
8
|
-
|
|
9
|
+
try {
|
|
10
|
+
return await authComponent.getAuthUser(ctx)
|
|
11
|
+
} catch {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
9
14
|
},
|
|
10
15
|
})
|
|
11
16
|
|
|
12
17
|
// Alias for current user - used by dashboard components
|
|
18
|
+
// Returns null if not authenticated (instead of throwing)
|
|
13
19
|
export const viewer = query({
|
|
14
20
|
args: {},
|
|
15
21
|
handler: async (ctx) => {
|
|
16
|
-
|
|
22
|
+
try {
|
|
23
|
+
return await authComponent.getAuthUser(ctx)
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
17
27
|
},
|
|
18
28
|
})
|
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
export default function HomePage() {
|
|
2
2
|
return (
|
|
3
3
|
<main className="min-h-screen">
|
|
4
|
+
{/* Navigation */}
|
|
5
|
+
<nav className="sticky top-0 z-50 w-full border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
|
|
6
|
+
<div className="container mx-auto px-4">
|
|
7
|
+
<div className="flex h-16 items-center justify-between">
|
|
8
|
+
<div className="flex items-center gap-2">
|
|
9
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 dark:bg-white">
|
|
10
|
+
<svg className="h-4 w-4 text-white dark:text-gray-900" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
11
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
12
|
+
</svg>
|
|
13
|
+
</div>
|
|
14
|
+
<span className="font-semibold text-gray-900 dark:text-white">{{projectName}}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div className="hidden md:flex items-center gap-6">
|
|
17
|
+
<a href="#features" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
18
|
+
Features
|
|
19
|
+
</a>
|
|
20
|
+
<a href="#" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
21
|
+
Pricing
|
|
22
|
+
</a>
|
|
23
|
+
<a href="#" className="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors">
|
|
24
|
+
Docs
|
|
25
|
+
</a>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="flex items-center gap-4">
|
|
28
|
+
<a
|
|
29
|
+
href="/sign-in"
|
|
30
|
+
className="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
|
|
31
|
+
>
|
|
32
|
+
Sign in
|
|
33
|
+
</a>
|
|
34
|
+
<a
|
|
35
|
+
href="/sign-up"
|
|
36
|
+
className="rounded-lg bg-gray-900 dark:bg-white px-4 py-2 text-sm font-medium text-white dark:text-gray-900 hover:opacity-90 transition-opacity"
|
|
37
|
+
>
|
|
38
|
+
Get Started
|
|
39
|
+
</a>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</nav>
|
|
44
|
+
|
|
4
45
|
{/* Hero Section */}
|
|
5
46
|
<section className="relative overflow-hidden bg-gradient-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-800">
|
|
6
47
|
<div className="container mx-auto px-4 py-24 sm:py-32">
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
SidebarMenu,
|
|
22
22
|
SidebarMenuButton,
|
|
23
23
|
SidebarMenuItem,
|
|
24
|
+
SidebarRail,
|
|
25
|
+
useSidebar,
|
|
24
26
|
} from '@/components/ui/sidebar'
|
|
25
27
|
import {
|
|
26
28
|
DropdownMenu,
|
|
@@ -47,6 +49,7 @@ const navigation = [
|
|
|
47
49
|
export function AppSidebar() {
|
|
48
50
|
const router = useRouter()
|
|
49
51
|
const { data: session } = useSession()
|
|
52
|
+
const { isMobile } = useSidebar()
|
|
50
53
|
const user = session?.user
|
|
51
54
|
|
|
52
55
|
const handleSignOut = async () => {
|
|
@@ -65,18 +68,18 @@ export function AppSidebar() {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
return (
|
|
68
|
-
<Sidebar>
|
|
71
|
+
<Sidebar collapsible="icon">
|
|
69
72
|
<SidebarHeader>
|
|
70
73
|
<SidebarMenu>
|
|
71
74
|
<SidebarMenuItem>
|
|
72
|
-
<SidebarMenuButton size="lg" asChild>
|
|
75
|
+
<SidebarMenuButton size="lg" asChild tooltip="{{projectName}}">
|
|
73
76
|
<a href="/">
|
|
74
|
-
<div className="flex aspect-square size-8 items-center justify-center rounded-lg
|
|
77
|
+
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
|
75
78
|
<GalleryVerticalEnd className="size-4" />
|
|
76
79
|
</div>
|
|
77
|
-
<div className="
|
|
78
|
-
<span className="font-semibold">{{projectName}}</span>
|
|
79
|
-
<span className="text-xs
|
|
80
|
+
<div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
|
81
|
+
<span className="truncate font-semibold">{{projectName}}</span>
|
|
82
|
+
<span className="truncate text-xs">Dashboard</span>
|
|
80
83
|
</div>
|
|
81
84
|
</a>
|
|
82
85
|
</SidebarMenuButton>
|
|
@@ -90,9 +93,9 @@ export function AppSidebar() {
|
|
|
90
93
|
<SidebarMenu>
|
|
91
94
|
{navigation.map((item) => (
|
|
92
95
|
<SidebarMenuItem key={item.title}>
|
|
93
|
-
<SidebarMenuButton asChild>
|
|
96
|
+
<SidebarMenuButton asChild tooltip={item.title}>
|
|
94
97
|
<a href={item.url}>
|
|
95
|
-
<item.icon
|
|
98
|
+
<item.icon />
|
|
96
99
|
<span>{item.title}</span>
|
|
97
100
|
</a>
|
|
98
101
|
</SidebarMenuButton>
|
|
@@ -119,7 +122,7 @@ export function AppSidebar() {
|
|
|
119
122
|
</Avatar>
|
|
120
123
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
121
124
|
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
|
|
122
|
-
<span className="truncate text-xs
|
|
125
|
+
<span className="truncate text-xs">
|
|
123
126
|
{user?.email ?? ''}
|
|
124
127
|
</span>
|
|
125
128
|
</div>
|
|
@@ -127,26 +130,26 @@ export function AppSidebar() {
|
|
|
127
130
|
</SidebarMenuButton>
|
|
128
131
|
</DropdownMenuTrigger>
|
|
129
132
|
<DropdownMenuContent
|
|
130
|
-
className="w-
|
|
131
|
-
side=
|
|
133
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
134
|
+
side={isMobile ? 'bottom' : 'right'}
|
|
132
135
|
align="end"
|
|
133
136
|
sideOffset={4}
|
|
134
137
|
>
|
|
135
138
|
<DropdownMenuItem asChild>
|
|
136
139
|
<a href="/settings">
|
|
137
|
-
<User
|
|
140
|
+
<User />
|
|
138
141
|
Profile
|
|
139
142
|
</a>
|
|
140
143
|
</DropdownMenuItem>
|
|
141
144
|
<DropdownMenuItem asChild>
|
|
142
145
|
<a href="/settings">
|
|
143
|
-
<Settings
|
|
146
|
+
<Settings />
|
|
144
147
|
Settings
|
|
145
148
|
</a>
|
|
146
149
|
</DropdownMenuItem>
|
|
147
150
|
<DropdownMenuSeparator />
|
|
148
151
|
<DropdownMenuItem onClick={handleSignOut}>
|
|
149
|
-
<LogOut
|
|
152
|
+
<LogOut />
|
|
150
153
|
Sign out
|
|
151
154
|
</DropdownMenuItem>
|
|
152
155
|
</DropdownMenuContent>
|
|
@@ -154,6 +157,7 @@ export function AppSidebar() {
|
|
|
154
157
|
</SidebarMenuItem>
|
|
155
158
|
</SidebarMenu>
|
|
156
159
|
</SidebarFooter>
|
|
160
|
+
<SidebarRail />
|
|
157
161
|
</Sidebar>
|
|
158
162
|
)
|
|
159
163
|
}
|
|
@@ -20,18 +20,20 @@ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayo
|
|
|
20
20
|
<SidebarProvider>
|
|
21
21
|
<AppSidebar />
|
|
22
22
|
<SidebarInset>
|
|
23
|
-
<header className="flex h-16 shrink-0 items-center gap-2
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
24
|
+
<div className="flex items-center gap-2 px-4">
|
|
25
|
+
<SidebarTrigger className="-ml-1" />
|
|
26
|
+
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
|
27
|
+
<Breadcrumb>
|
|
28
|
+
<BreadcrumbList>
|
|
29
|
+
<BreadcrumbItem>
|
|
30
|
+
<BreadcrumbPage>{title}</BreadcrumbPage>
|
|
31
|
+
</BreadcrumbItem>
|
|
32
|
+
</BreadcrumbList>
|
|
33
|
+
</Breadcrumb>
|
|
34
|
+
</div>
|
|
33
35
|
</header>
|
|
34
|
-
<main className="flex-1
|
|
36
|
+
<main className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</main>
|
|
35
37
|
</SidebarInset>
|
|
36
38
|
</SidebarProvider>
|
|
37
39
|
)
|