kofi-stack-template-generator 2.0.15 → 2.0.17
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/.turbo/turbo-build.log +6 -6
- package/dist/index.js +616 -56
- package/package.json +8 -8
- package/src/generator.ts +32 -3
- package/src/templates.generated.ts +17 -9
- package/templates/convex/_env.local.hbs +31 -5
- package/templates/convex/convex/auth.ts.hbs +28 -1
- package/templates/convex/convex/users.ts.hbs +12 -0
- package/templates/convex/package.json.hbs +7 -1
- package/templates/marketing/payload/_env.local.hbs +7 -7
- package/templates/web/src/app/(auth)/layout.tsx.hbs +13 -0
- package/templates/web/src/app/(auth)/sign-in/page.tsx.hbs +5 -0
- package/templates/web/src/app/(auth)/sign-up/page.tsx.hbs +5 -0
- package/templates/web/src/app/page.tsx.hbs +101 -47
- package/templates/web/src/components/auth/sign-in-form.tsx.hbs +121 -0
- package/templates/web/src/components/auth/sign-up-form.tsx.hbs +141 -0
- package/templates/web/src/components/dashboard/app-sidebar.tsx.hbs +163 -0
- package/templates/web/src/components/dashboard/dashboard-layout.tsx.hbs +38 -0
- package/templates/web/src/lib/auth.ts.hbs +13 -3
- package/templates/web/src/proxy.ts.hbs +21 -0
package/dist/index.js
CHANGED
|
@@ -243,11 +243,11 @@ function shouldIncludeFile(templatePath, config) {
|
|
|
243
243
|
var EMBEDDED_TEMPLATES = {
|
|
244
244
|
"base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
|
|
245
245
|
"base/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "organizeImports": {\n "enabled": true\n },\n "linter": {\n "enabled": true,\n "rules": {\n "recommended": true\n }\n },\n "formatter": {\n "enabled": true,\n "indentStyle": "space",\n "indentWidth": 2\n },\n "javascript": {\n "formatter": {\n "quoteStyle": "single",\n "semicolons": "asNeeded"\n }\n },\n "files": {\n "ignore": [\n "node_modules",\n ".next",\n "dist",\n ".turbo",\n "coverage",\n ".vercel",\n "_generated"\n ]\n }\n}\n',
|
|
246
|
-
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n#
|
|
247
|
-
"convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [GitHub
|
|
246
|
+
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n\n# Email (Resend) - https://resend.com\nRESEND_API_KEY=\nRESEND_FROM_EMAIL=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'convex-fs')}}\n\n# Convex FS - Built-in file storage (no additional config needed)\n{{/if}}\n{{#if (eq integrations.uploads 'r2')}}\n\n# Cloudflare R2\nR2_ACCESS_KEY_ID=\nR2_SECRET_ACCESS_KEY=\nR2_BUCKET=\nR2_ENDPOINT=\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (eq integrations.payments 'stripe')}}\n\n# Stripe\nSTRIPE_SECRET_KEY=\nSTRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n{{/if}}\n{{#if (eq integrations.payments 'polar')}}\n\n# Polar\nPOLAR_ACCESS_TOKEN=\nPOLAR_WEBHOOK_SECRET=\nPOLAR_ORGANIZATION_ID=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Convex Rate Limiter - No additional config needed (uses Convex backend)\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n",
|
|
247
|
+
"convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { Password } from '@convex-dev/auth/providers/Password'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [\n GitHub,\n Google,\n Password,\n ],\n})\n\n// Email Verification Setup (Optional)\n// ------------------------------------\n// By default, email/password auth works without email verification.\n// To require email verification, configure the Password provider with ResendOTP:\n//\n// 1. Install the Resend component: npm install @convex-dev/resend\n// 2. Update the Password provider:\n//\n// import { ResendOTP } from '@convex-dev/auth/providers/ResendOTP'\n// import { Resend } from '@convex-dev/resend'\n//\n// const resend = new Resend()\n//\n// Password({\n// verify: ResendOTP({\n// resend,\n// from: process.env.RESEND_FROM_EMAIL!,\n// }),\n// })\n//\n// See: https://labs.convex.dev/auth/config/passwords\n",
|
|
248
248
|
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\nauth.addHttpRoutes(http)\n\nexport default http\n",
|
|
249
249
|
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { authTables } from '@convex-dev/auth/server'\nimport { v } from 'convex/values'\n\nexport default defineSchema({\n ...authTables,\n // Add your custom tables here\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // authorId: v.id('users'),\n // createdAt: v.number(),\n // }).index('by_author', ['authorId']),\n})\n",
|
|
250
|
-
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
|
|
250
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n\n// Alias for current user - used by dashboard components\nexport const viewer = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
|
|
251
251
|
"convex/package.json.hbs": `{{#if (eq structure 'monorepo')}}{
|
|
252
252
|
"name": "@repo/backend",
|
|
253
253
|
"version": "0.1.0",
|
|
@@ -269,7 +269,13 @@ var EMBEDDED_TEMPLATES = {
|
|
|
269
269
|
"dependencies": {
|
|
270
270
|
"convex": "^1.25.0",
|
|
271
271
|
"@convex-dev/auth": "^0.0.90",
|
|
272
|
-
"@auth/core": "^0.37.0"
|
|
272
|
+
"@auth/core": "^0.37.0",
|
|
273
|
+
"@convex-dev/resend": "^0.1.0"{{#if (eq integrations.uploads 'convex-fs')}},
|
|
274
|
+
"@convex-dev/convex-fs": "^0.1.0"{{/if}}{{#if (eq integrations.uploads 'r2')}},
|
|
275
|
+
"@convex-dev/cloudflare-r2": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'stripe')}},
|
|
276
|
+
"@convex-dev/stripe": "^0.1.0"{{/if}}{{#if (eq integrations.payments 'polar')}},
|
|
277
|
+
"@convex-dev/polar": "^0.1.0"{{/if}}{{#if (includes addons 'rate-limiting')}},
|
|
278
|
+
"@convex-dev/rate-limiter": "^0.1.0"{{/if}}
|
|
273
279
|
},
|
|
274
280
|
"devDependencies": {
|
|
275
281
|
"typescript": "^5.0.0"
|
|
@@ -427,7 +433,7 @@ var EMBEDDED_TEMPLATES = {
|
|
|
427
433
|
`,
|
|
428
434
|
"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',
|
|
429
435
|
"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',
|
|
430
|
-
"marketing/payload/_env.local.hbs": '# Database (Supabase PostgreSQL)\nDATABASE_URL
|
|
436
|
+
"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',
|
|
431
437
|
"marketing/payload/next.config.ts.hbs": "import { withPayload } from '@payloadcms/next/withPayload'\nimport type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n transpilePackages: ['@repo/ui'],\n experimental: {\n turbo: {},\n },\n}\n\nexport default withPayload(nextConfig)\n",
|
|
432
438
|
"marketing/payload/package.json.hbs": '{\n "name": "@repo/marketing",\n "version": "0.1.0",\n "private": true,\n "scripts": {\n "dev": "next dev --turbopack -p 3001",\n "build": "next build",\n "start": "next start",\n "lint": "biome check .",\n "lint:fix": "biome check --write .",\n "typecheck": "tsc --noEmit",\n "db:push": "payload migrate",\n "db:seed": "tsx src/seed.ts"\n },\n "dependencies": {\n "next": "^15.4.10",\n "react": "^19.0.0",\n "react-dom": "^19.0.0",\n "payload": "^3.70.0",\n "@payloadcms/db-postgres": "^3.0.0",\n "@payloadcms/next": "^3.0.0",\n "@payloadcms/richtext-lexical": "^3.0.0",\n "@payloadcms/storage-s3": "^3.0.0",\n "@payloadcms/plugin-seo": "^3.0.0",\n "@repo/ui": "workspace:*"\n },\n "devDependencies": {\n "@repo/config-typescript": "workspace:*",\n "@types/node": "^20.0.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "tailwindcss": "^4.0.0",\n "@tailwindcss/postcss": "^4.0.0",\n "postcss": "^8.4.0",\n "sass": "^1.86.0",\n "typescript": "^5.0.0",\n "tsx": "^4.0.0"\n }\n}\n',
|
|
433
439
|
"marketing/payload/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
@@ -645,66 +651,123 @@ export default function RootLayout({
|
|
|
645
651
|
}
|
|
646
652
|
`,
|
|
647
653
|
"web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
654
|
+
"web/src/app/(auth)/layout.tsx.hbs": 'export default function AuthLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <div className="min-h-screen flex items-center justify-center bg-muted/50 p-4">\n <div className="w-full max-w-md">\n {children}\n </div>\n </div>\n )\n}\n',
|
|
655
|
+
"web/src/app/(auth)/sign-in/page.tsx.hbs": "import { SignInForm } from '@/components/auth/sign-in-form'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n",
|
|
656
|
+
"web/src/app/(auth)/sign-up/page.tsx.hbs": "import { SignUpForm } from '@/components/auth/sign-up-form'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n",
|
|
648
657
|
"web/src/app/globals.css.hbs": '@import "tailwindcss";\n@import "tw-animate-css";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n',
|
|
649
658
|
"web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\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}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
|
|
650
659
|
"web/src/app/page.tsx.hbs": `'use client'
|
|
651
660
|
|
|
652
|
-
import {
|
|
661
|
+
import { useQuery } from 'convex/react'
|
|
662
|
+
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'
|
|
663
|
+
import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
|
|
664
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
665
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
653
666
|
|
|
654
667
|
export default function HomePage() {
|
|
655
|
-
const
|
|
668
|
+
const user = useQuery(api.users.viewer)
|
|
656
669
|
|
|
657
|
-
if (
|
|
670
|
+
if (user === undefined) {
|
|
658
671
|
return (
|
|
659
|
-
<
|
|
660
|
-
<div className="
|
|
661
|
-
|
|
672
|
+
<DashboardLayout title="Dashboard">
|
|
673
|
+
<div className="space-y-6">
|
|
674
|
+
<Skeleton className="h-8 w-64" />
|
|
675
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
676
|
+
<Skeleton className="h-32" />
|
|
677
|
+
<Skeleton className="h-32" />
|
|
678
|
+
<Skeleton className="h-32" />
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</DashboardLayout>
|
|
662
682
|
)
|
|
663
683
|
}
|
|
664
684
|
|
|
665
685
|
return (
|
|
666
|
-
<
|
|
667
|
-
<div className="
|
|
668
|
-
<
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
686
|
+
<DashboardLayout title="Dashboard">
|
|
687
|
+
<div className="space-y-6">
|
|
688
|
+
<div>
|
|
689
|
+
<h1 className="text-3xl font-bold tracking-tight">
|
|
690
|
+
Welcome back{user?.name ? \`, \${user.name}\` : ''}!
|
|
691
|
+
</h1>
|
|
692
|
+
<p className="text-muted-foreground">
|
|
693
|
+
Here's what's happening with your project today.
|
|
694
|
+
</p>
|
|
695
|
+
</div>
|
|
672
696
|
|
|
673
|
-
|
|
674
|
-
<
|
|
675
|
-
<
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
className="
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
<
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
>
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
697
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
698
|
+
<Card>
|
|
699
|
+
<CardHeader>
|
|
700
|
+
<CardTitle>Getting Started</CardTitle>
|
|
701
|
+
<CardDescription>Quick start guide for your app</CardDescription>
|
|
702
|
+
</CardHeader>
|
|
703
|
+
<CardContent>
|
|
704
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
705
|
+
<li>Customize your dashboard layout</li>
|
|
706
|
+
<li>Add new pages to the sidebar</li>
|
|
707
|
+
<li>Connect your data sources</li>
|
|
708
|
+
</ul>
|
|
709
|
+
</CardContent>
|
|
710
|
+
</Card>
|
|
711
|
+
|
|
712
|
+
<Card>
|
|
713
|
+
<CardHeader>
|
|
714
|
+
<CardTitle>Documentation</CardTitle>
|
|
715
|
+
<CardDescription>Learn how to build with Kofi Stack</CardDescription>
|
|
716
|
+
</CardHeader>
|
|
717
|
+
<CardContent>
|
|
718
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
719
|
+
<li>
|
|
720
|
+
<a
|
|
721
|
+
href="https://docs.convex.dev"
|
|
722
|
+
target="_blank"
|
|
723
|
+
rel="noopener noreferrer"
|
|
724
|
+
className="text-primary hover:underline"
|
|
725
|
+
>
|
|
726
|
+
Convex Documentation
|
|
727
|
+
</a>
|
|
728
|
+
</li>
|
|
729
|
+
<li>
|
|
730
|
+
<a
|
|
731
|
+
href="https://ui.shadcn.com"
|
|
732
|
+
target="_blank"
|
|
733
|
+
rel="noopener noreferrer"
|
|
734
|
+
className="text-primary hover:underline"
|
|
735
|
+
>
|
|
736
|
+
shadcn/ui Components
|
|
737
|
+
</a>
|
|
738
|
+
</li>
|
|
739
|
+
<li>
|
|
740
|
+
<a
|
|
741
|
+
href="https://nextjs.org/docs"
|
|
742
|
+
target="_blank"
|
|
743
|
+
rel="noopener noreferrer"
|
|
744
|
+
className="text-primary hover:underline"
|
|
745
|
+
>
|
|
746
|
+
Next.js Documentation
|
|
747
|
+
</a>
|
|
748
|
+
</li>
|
|
749
|
+
</ul>
|
|
750
|
+
</CardContent>
|
|
751
|
+
</Card>
|
|
706
752
|
|
|
707
|
-
|
|
753
|
+
<Card>
|
|
754
|
+
<CardHeader>
|
|
755
|
+
<CardTitle>Your Stack</CardTitle>
|
|
756
|
+
<CardDescription>Technologies powering your app</CardDescription>
|
|
757
|
+
</CardHeader>
|
|
758
|
+
<CardContent>
|
|
759
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
760
|
+
<li>Next.js 15 with App Router</li>
|
|
761
|
+
<li>Convex for backend & database</li>
|
|
762
|
+
<li>Convex Auth for authentication</li>
|
|
763
|
+
<li>shadcn/ui components</li>
|
|
764
|
+
<li>Tailwind CSS for styling</li>
|
|
765
|
+
</ul>
|
|
766
|
+
</CardContent>
|
|
767
|
+
</Card>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<div className="pt-4 border-t">
|
|
708
771
|
<p className="text-sm text-muted-foreground">
|
|
709
772
|
Created with{' '}
|
|
710
773
|
<a
|
|
@@ -718,13 +781,481 @@ export default function HomePage() {
|
|
|
718
781
|
</p>
|
|
719
782
|
</div>
|
|
720
783
|
</div>
|
|
721
|
-
</
|
|
784
|
+
</DashboardLayout>
|
|
785
|
+
)
|
|
786
|
+
}
|
|
787
|
+
`,
|
|
788
|
+
"web/src/components/auth/sign-in-form.tsx.hbs": `'use client'
|
|
789
|
+
|
|
790
|
+
import { useState } from 'react'
|
|
791
|
+
import { useRouter } from 'next/navigation'
|
|
792
|
+
import Link from 'next/link'
|
|
793
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
794
|
+
import { Button } from '@/components/ui/button'
|
|
795
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
796
|
+
import { Input } from '@/components/ui/input'
|
|
797
|
+
import { Label } from '@/components/ui/label'
|
|
798
|
+
import { Separator } from '@/components/ui/separator'
|
|
799
|
+
|
|
800
|
+
export function SignInForm() {
|
|
801
|
+
const router = useRouter()
|
|
802
|
+
const { signIn } = useAuthActions()
|
|
803
|
+
const [email, setEmail] = useState('')
|
|
804
|
+
const [password, setPassword] = useState('')
|
|
805
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
806
|
+
const [error, setError] = useState<string | null>(null)
|
|
807
|
+
|
|
808
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
809
|
+
e.preventDefault()
|
|
810
|
+
setIsLoading(true)
|
|
811
|
+
setError(null)
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
const formData = new FormData()
|
|
815
|
+
formData.append('email', email)
|
|
816
|
+
formData.append('password', password)
|
|
817
|
+
formData.append('flow', 'signIn')
|
|
818
|
+
|
|
819
|
+
await signIn('password', formData)
|
|
820
|
+
router.push('/')
|
|
821
|
+
} catch (err) {
|
|
822
|
+
setError('Invalid email or password')
|
|
823
|
+
} finally {
|
|
824
|
+
setIsLoading(false)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
829
|
+
void signIn(provider)
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return (
|
|
833
|
+
<Card>
|
|
834
|
+
<CardHeader className="text-center">
|
|
835
|
+
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
|
836
|
+
<CardDescription>Sign in to your account to continue</CardDescription>
|
|
837
|
+
</CardHeader>
|
|
838
|
+
<CardContent>
|
|
839
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
840
|
+
<div className="space-y-2">
|
|
841
|
+
<Label htmlFor="email">Email</Label>
|
|
842
|
+
<Input
|
|
843
|
+
id="email"
|
|
844
|
+
type="email"
|
|
845
|
+
placeholder="you@example.com"
|
|
846
|
+
value={email}
|
|
847
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
848
|
+
required
|
|
849
|
+
/>
|
|
850
|
+
</div>
|
|
851
|
+
<div className="space-y-2">
|
|
852
|
+
<Label htmlFor="password">Password</Label>
|
|
853
|
+
<Input
|
|
854
|
+
id="password"
|
|
855
|
+
type="password"
|
|
856
|
+
placeholder="Enter your password"
|
|
857
|
+
value={password}
|
|
858
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
859
|
+
required
|
|
860
|
+
/>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
{error && (
|
|
864
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
865
|
+
)}
|
|
866
|
+
|
|
867
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
868
|
+
{isLoading ? 'Signing in...' : 'Sign In'}
|
|
869
|
+
</Button>
|
|
870
|
+
</form>
|
|
871
|
+
|
|
872
|
+
<div className="relative my-6">
|
|
873
|
+
<div className="absolute inset-0 flex items-center">
|
|
874
|
+
<Separator className="w-full" />
|
|
875
|
+
</div>
|
|
876
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
877
|
+
<span className="bg-card px-2 text-muted-foreground">Or continue with</span>
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div className="grid grid-cols-2 gap-4">
|
|
882
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('github')}>
|
|
883
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
884
|
+
<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"/>
|
|
885
|
+
</svg>
|
|
886
|
+
GitHub
|
|
887
|
+
</Button>
|
|
888
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('google')}>
|
|
889
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
890
|
+
<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"/>
|
|
891
|
+
<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"/>
|
|
892
|
+
<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"/>
|
|
893
|
+
<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"/>
|
|
894
|
+
</svg>
|
|
895
|
+
Google
|
|
896
|
+
</Button>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
900
|
+
Don't have an account?{' '}
|
|
901
|
+
<Link href="/sign-up" className="text-primary hover:underline font-medium">
|
|
902
|
+
Sign up
|
|
903
|
+
</Link>
|
|
904
|
+
</p>
|
|
905
|
+
</CardContent>
|
|
906
|
+
</Card>
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
`,
|
|
910
|
+
"web/src/components/auth/sign-up-form.tsx.hbs": `'use client'
|
|
911
|
+
|
|
912
|
+
import { useState } from 'react'
|
|
913
|
+
import { useRouter } from 'next/navigation'
|
|
914
|
+
import Link from 'next/link'
|
|
915
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
916
|
+
import { Button } from '@/components/ui/button'
|
|
917
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
918
|
+
import { Input } from '@/components/ui/input'
|
|
919
|
+
import { Label } from '@/components/ui/label'
|
|
920
|
+
import { Separator } from '@/components/ui/separator'
|
|
921
|
+
|
|
922
|
+
export function SignUpForm() {
|
|
923
|
+
const router = useRouter()
|
|
924
|
+
const { signIn } = useAuthActions()
|
|
925
|
+
const [name, setName] = useState('')
|
|
926
|
+
const [email, setEmail] = useState('')
|
|
927
|
+
const [password, setPassword] = useState('')
|
|
928
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
929
|
+
const [error, setError] = useState<string | null>(null)
|
|
930
|
+
|
|
931
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
932
|
+
e.preventDefault()
|
|
933
|
+
setIsLoading(true)
|
|
934
|
+
setError(null)
|
|
935
|
+
|
|
936
|
+
if (password.length < 8) {
|
|
937
|
+
setError('Password must be at least 8 characters')
|
|
938
|
+
setIsLoading(false)
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
const formData = new FormData()
|
|
944
|
+
formData.append('name', name)
|
|
945
|
+
formData.append('email', email)
|
|
946
|
+
formData.append('password', password)
|
|
947
|
+
formData.append('flow', 'signUp')
|
|
948
|
+
|
|
949
|
+
await signIn('password', formData)
|
|
950
|
+
router.push('/')
|
|
951
|
+
} catch (err) {
|
|
952
|
+
setError('Failed to create account. Email may already be in use.')
|
|
953
|
+
} finally {
|
|
954
|
+
setIsLoading(false)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
959
|
+
void signIn(provider)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return (
|
|
963
|
+
<Card>
|
|
964
|
+
<CardHeader className="text-center">
|
|
965
|
+
<CardTitle className="text-2xl">Create an account</CardTitle>
|
|
966
|
+
<CardDescription>Enter your details to get started</CardDescription>
|
|
967
|
+
</CardHeader>
|
|
968
|
+
<CardContent>
|
|
969
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
970
|
+
<div className="space-y-2">
|
|
971
|
+
<Label htmlFor="name">Name</Label>
|
|
972
|
+
<Input
|
|
973
|
+
id="name"
|
|
974
|
+
type="text"
|
|
975
|
+
placeholder="Your name"
|
|
976
|
+
value={name}
|
|
977
|
+
onChange={(e) => setName(e.target.value)}
|
|
978
|
+
required
|
|
979
|
+
/>
|
|
980
|
+
</div>
|
|
981
|
+
<div className="space-y-2">
|
|
982
|
+
<Label htmlFor="email">Email</Label>
|
|
983
|
+
<Input
|
|
984
|
+
id="email"
|
|
985
|
+
type="email"
|
|
986
|
+
placeholder="you@example.com"
|
|
987
|
+
value={email}
|
|
988
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
989
|
+
required
|
|
990
|
+
/>
|
|
991
|
+
</div>
|
|
992
|
+
<div className="space-y-2">
|
|
993
|
+
<Label htmlFor="password">Password</Label>
|
|
994
|
+
<Input
|
|
995
|
+
id="password"
|
|
996
|
+
type="password"
|
|
997
|
+
placeholder="At least 8 characters"
|
|
998
|
+
value={password}
|
|
999
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
1000
|
+
required
|
|
1001
|
+
minLength={8}
|
|
1002
|
+
/>
|
|
1003
|
+
</div>
|
|
1004
|
+
|
|
1005
|
+
{error && (
|
|
1006
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
1007
|
+
)}
|
|
1008
|
+
|
|
1009
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
1010
|
+
{isLoading ? 'Creating account...' : 'Create Account'}
|
|
1011
|
+
</Button>
|
|
1012
|
+
</form>
|
|
1013
|
+
|
|
1014
|
+
<div className="relative my-6">
|
|
1015
|
+
<div className="absolute inset-0 flex items-center">
|
|
1016
|
+
<Separator className="w-full" />
|
|
1017
|
+
</div>
|
|
1018
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
1019
|
+
<span className="bg-card px-2 text-muted-foreground">Or continue with</span>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
|
|
1023
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1024
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('github')}>
|
|
1025
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
1026
|
+
<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"/>
|
|
1027
|
+
</svg>
|
|
1028
|
+
GitHub
|
|
1029
|
+
</Button>
|
|
1030
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('google')}>
|
|
1031
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
1032
|
+
<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"/>
|
|
1033
|
+
<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"/>
|
|
1034
|
+
<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"/>
|
|
1035
|
+
<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"/>
|
|
1036
|
+
</svg>
|
|
1037
|
+
Google
|
|
1038
|
+
</Button>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
1041
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
1042
|
+
Already have an account?{' '}
|
|
1043
|
+
<Link href="/sign-in" className="text-primary hover:underline font-medium">
|
|
1044
|
+
Sign in
|
|
1045
|
+
</Link>
|
|
1046
|
+
</p>
|
|
1047
|
+
</CardContent>
|
|
1048
|
+
</Card>
|
|
1049
|
+
)
|
|
1050
|
+
}
|
|
1051
|
+
`,
|
|
1052
|
+
"web/src/components/dashboard/app-sidebar.tsx.hbs": `'use client'
|
|
1053
|
+
|
|
1054
|
+
import {
|
|
1055
|
+
AudioWaveform,
|
|
1056
|
+
Command,
|
|
1057
|
+
GalleryVerticalEnd,
|
|
1058
|
+
Home,
|
|
1059
|
+
Settings,
|
|
1060
|
+
ChevronsUpDown,
|
|
1061
|
+
LogOut,
|
|
1062
|
+
User,
|
|
1063
|
+
} from 'lucide-react'
|
|
1064
|
+
import { useRouter } from 'next/navigation'
|
|
1065
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
1066
|
+
import { useQuery } from 'convex/react'
|
|
1067
|
+
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
1068
|
+
import {
|
|
1069
|
+
Sidebar,
|
|
1070
|
+
SidebarContent,
|
|
1071
|
+
SidebarFooter,
|
|
1072
|
+
SidebarGroup,
|
|
1073
|
+
SidebarGroupContent,
|
|
1074
|
+
SidebarGroupLabel,
|
|
1075
|
+
SidebarHeader,
|
|
1076
|
+
SidebarMenu,
|
|
1077
|
+
SidebarMenuButton,
|
|
1078
|
+
SidebarMenuItem,
|
|
1079
|
+
} from '@/components/ui/sidebar'
|
|
1080
|
+
import {
|
|
1081
|
+
DropdownMenu,
|
|
1082
|
+
DropdownMenuContent,
|
|
1083
|
+
DropdownMenuItem,
|
|
1084
|
+
DropdownMenuSeparator,
|
|
1085
|
+
DropdownMenuTrigger,
|
|
1086
|
+
} from '@/components/ui/dropdown-menu'
|
|
1087
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
1088
|
+
|
|
1089
|
+
const navigation = [
|
|
1090
|
+
{
|
|
1091
|
+
title: 'Home',
|
|
1092
|
+
url: '/',
|
|
1093
|
+
icon: Home,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
title: 'Settings',
|
|
1097
|
+
url: '/settings',
|
|
1098
|
+
icon: Settings,
|
|
1099
|
+
},
|
|
1100
|
+
]
|
|
1101
|
+
|
|
1102
|
+
export function AppSidebar() {
|
|
1103
|
+
const router = useRouter()
|
|
1104
|
+
const { signOut } = useAuthActions()
|
|
1105
|
+
const user = useQuery(api.users.viewer)
|
|
1106
|
+
|
|
1107
|
+
const handleSignOut = async () => {
|
|
1108
|
+
await signOut()
|
|
1109
|
+
router.push('/sign-in')
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const getInitials = (name?: string | null) => {
|
|
1113
|
+
if (!name) return 'U'
|
|
1114
|
+
return name
|
|
1115
|
+
.split(' ')
|
|
1116
|
+
.map((n) => n[0])
|
|
1117
|
+
.join('')
|
|
1118
|
+
.toUpperCase()
|
|
1119
|
+
.slice(0, 2)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return (
|
|
1123
|
+
<Sidebar>
|
|
1124
|
+
<SidebarHeader>
|
|
1125
|
+
<SidebarMenu>
|
|
1126
|
+
<SidebarMenuItem>
|
|
1127
|
+
<SidebarMenuButton size="lg" asChild>
|
|
1128
|
+
<a href="/">
|
|
1129
|
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
1130
|
+
<GalleryVerticalEnd className="size-4" />
|
|
1131
|
+
</div>
|
|
1132
|
+
<div className="flex flex-col gap-0.5 leading-none">
|
|
1133
|
+
<span className="font-semibold">{{projectName}}</span>
|
|
1134
|
+
<span className="text-xs text-muted-foreground">Dashboard</span>
|
|
1135
|
+
</div>
|
|
1136
|
+
</a>
|
|
1137
|
+
</SidebarMenuButton>
|
|
1138
|
+
</SidebarMenuItem>
|
|
1139
|
+
</SidebarMenu>
|
|
1140
|
+
</SidebarHeader>
|
|
1141
|
+
<SidebarContent>
|
|
1142
|
+
<SidebarGroup>
|
|
1143
|
+
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
1144
|
+
<SidebarGroupContent>
|
|
1145
|
+
<SidebarMenu>
|
|
1146
|
+
{navigation.map((item) => (
|
|
1147
|
+
<SidebarMenuItem key={item.title}>
|
|
1148
|
+
<SidebarMenuButton asChild>
|
|
1149
|
+
<a href={item.url}>
|
|
1150
|
+
<item.icon className="size-4" />
|
|
1151
|
+
<span>{item.title}</span>
|
|
1152
|
+
</a>
|
|
1153
|
+
</SidebarMenuButton>
|
|
1154
|
+
</SidebarMenuItem>
|
|
1155
|
+
))}
|
|
1156
|
+
</SidebarMenu>
|
|
1157
|
+
</SidebarGroupContent>
|
|
1158
|
+
</SidebarGroup>
|
|
1159
|
+
</SidebarContent>
|
|
1160
|
+
<SidebarFooter>
|
|
1161
|
+
<SidebarMenu>
|
|
1162
|
+
<SidebarMenuItem>
|
|
1163
|
+
<DropdownMenu>
|
|
1164
|
+
<DropdownMenuTrigger asChild>
|
|
1165
|
+
<SidebarMenuButton
|
|
1166
|
+
size="lg"
|
|
1167
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
1168
|
+
>
|
|
1169
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
1170
|
+
<AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />
|
|
1171
|
+
<AvatarFallback className="rounded-lg">
|
|
1172
|
+
{getInitials(user?.name)}
|
|
1173
|
+
</AvatarFallback>
|
|
1174
|
+
</Avatar>
|
|
1175
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
1176
|
+
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
|
|
1177
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
1178
|
+
{user?.email ?? ''}
|
|
1179
|
+
</span>
|
|
1180
|
+
</div>
|
|
1181
|
+
<ChevronsUpDown className="ml-auto size-4" />
|
|
1182
|
+
</SidebarMenuButton>
|
|
1183
|
+
</DropdownMenuTrigger>
|
|
1184
|
+
<DropdownMenuContent
|
|
1185
|
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
1186
|
+
side="bottom"
|
|
1187
|
+
align="end"
|
|
1188
|
+
sideOffset={4}
|
|
1189
|
+
>
|
|
1190
|
+
<DropdownMenuItem asChild>
|
|
1191
|
+
<a href="/settings">
|
|
1192
|
+
<User className="mr-2 size-4" />
|
|
1193
|
+
Profile
|
|
1194
|
+
</a>
|
|
1195
|
+
</DropdownMenuItem>
|
|
1196
|
+
<DropdownMenuItem asChild>
|
|
1197
|
+
<a href="/settings">
|
|
1198
|
+
<Settings className="mr-2 size-4" />
|
|
1199
|
+
Settings
|
|
1200
|
+
</a>
|
|
1201
|
+
</DropdownMenuItem>
|
|
1202
|
+
<DropdownMenuSeparator />
|
|
1203
|
+
<DropdownMenuItem onClick={handleSignOut}>
|
|
1204
|
+
<LogOut className="mr-2 size-4" />
|
|
1205
|
+
Sign out
|
|
1206
|
+
</DropdownMenuItem>
|
|
1207
|
+
</DropdownMenuContent>
|
|
1208
|
+
</DropdownMenu>
|
|
1209
|
+
</SidebarMenuItem>
|
|
1210
|
+
</SidebarMenu>
|
|
1211
|
+
</SidebarFooter>
|
|
1212
|
+
</Sidebar>
|
|
1213
|
+
)
|
|
1214
|
+
}
|
|
1215
|
+
`,
|
|
1216
|
+
"web/src/components/dashboard/dashboard-layout.tsx.hbs": `'use client'
|
|
1217
|
+
|
|
1218
|
+
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
|
1219
|
+
import { Separator } from '@/components/ui/separator'
|
|
1220
|
+
import {
|
|
1221
|
+
Breadcrumb,
|
|
1222
|
+
BreadcrumbItem,
|
|
1223
|
+
BreadcrumbList,
|
|
1224
|
+
BreadcrumbPage,
|
|
1225
|
+
} from '@/components/ui/breadcrumb'
|
|
1226
|
+
import { AppSidebar } from './app-sidebar'
|
|
1227
|
+
|
|
1228
|
+
interface DashboardLayoutProps {
|
|
1229
|
+
children: React.ReactNode
|
|
1230
|
+
title?: string
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {
|
|
1234
|
+
return (
|
|
1235
|
+
<SidebarProvider>
|
|
1236
|
+
<AppSidebar />
|
|
1237
|
+
<SidebarInset>
|
|
1238
|
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
|
1239
|
+
<SidebarTrigger className="-ml-1" />
|
|
1240
|
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
1241
|
+
<Breadcrumb>
|
|
1242
|
+
<BreadcrumbList>
|
|
1243
|
+
<BreadcrumbItem>
|
|
1244
|
+
<BreadcrumbPage>{title}</BreadcrumbPage>
|
|
1245
|
+
</BreadcrumbItem>
|
|
1246
|
+
</BreadcrumbList>
|
|
1247
|
+
</Breadcrumb>
|
|
1248
|
+
</header>
|
|
1249
|
+
<main className="flex-1 p-4 md:p-6">{children}</main>
|
|
1250
|
+
</SidebarInset>
|
|
1251
|
+
</SidebarProvider>
|
|
722
1252
|
)
|
|
723
1253
|
}
|
|
724
1254
|
`,
|
|
725
1255
|
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs'\nimport { ConvexReactClient } from 'convex/react'\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 <ConvexAuthNextjsProvider client={convex}>\n {children}\n </ConvexAuthNextjsProvider>\n )\n}\n",
|
|
726
|
-
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth,
|
|
1256
|
+
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth, useQuery } from 'convex/react'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'\n\nexport function useAuth() {\n const { isAuthenticated, isLoading } = useConvexAuth()\n const { signIn, signOut } = useAuthActions()\n const user = useQuery(api.users.viewer)\n\n return {\n isAuthenticated,\n isLoading,\n user,\n signIn: (provider: 'github' | 'google') => {\n void signIn(provider)\n },\n signInWithPassword: async (email: string, password: string, flow: 'signIn' | 'signUp' = 'signIn', name?: string) => {\n const formData = new FormData()\n formData.append('email', email)\n formData.append('password', password)\n formData.append('flow', flow)\n if (name) {\n formData.append('name', name)\n }\n await signIn('password', formData)\n },\n signOut: () => {\n void signOut()\n },\n }\n}\n",
|
|
727
1257
|
"web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
1258
|
+
"web/src/proxy.ts.hbs": "import {\n convexAuthNextjsProxy,\n createRouteMatcher,\n nextjsProxyRedirect,\n} from '@convex-dev/auth/nextjs/server'\n\nconst isPublicRoute = createRouteMatcher(['/sign-in', '/sign-up'])\n\nexport default convexAuthNextjsProxy(async (request, { convexAuth }) => {\n const isAuthenticated = await convexAuth.isAuthenticated()\n\n // Redirect unauthenticated users to /sign-up\n if (!isPublicRoute(request) && !isAuthenticated) {\n return nextjsProxyRedirect(request, '/sign-up')\n }\n\n // Redirect authenticated users from auth pages to / (dashboard)\n if (isPublicRoute(request) && isAuthenticated) {\n return nextjsProxyRedirect(request, '/')\n }\n})\n",
|
|
728
1259
|
"web/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'
|
|
729
1260
|
};
|
|
730
1261
|
|
|
@@ -843,19 +1374,21 @@ async function postProcess(vfs, config) {
|
|
|
843
1374
|
function generateDevScript(vfs, scriptsPath, config) {
|
|
844
1375
|
const isMonorepo = config.structure === "monorepo";
|
|
845
1376
|
const webAppDir = isMonorepo ? "apps/web" : ".";
|
|
1377
|
+
const backendDir = isMonorepo ? "packages/backend" : ".";
|
|
846
1378
|
const devScript = `#!/usr/bin/env node
|
|
847
1379
|
/**
|
|
848
1380
|
* Dev Script - Starts Next.js and Convex dev servers
|
|
849
1381
|
*/
|
|
850
1382
|
|
|
851
1383
|
import { spawn, execSync } from 'child_process'
|
|
852
|
-
import { existsSync, readFileSync } from 'fs'
|
|
1384
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
853
1385
|
import { resolve, dirname } from 'path'
|
|
854
1386
|
import { fileURLToPath } from 'url'
|
|
855
1387
|
|
|
856
1388
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
857
1389
|
const rootDir = resolve(__dirname, '..')
|
|
858
1390
|
const webAppDir = resolve(rootDir, '${webAppDir}')
|
|
1391
|
+
const backendDir = resolve(rootDir, '${backendDir}')
|
|
859
1392
|
|
|
860
1393
|
function loadEnvFile(dir) {
|
|
861
1394
|
const envPath = resolve(dir, '.env.local')
|
|
@@ -880,6 +1413,33 @@ function loadEnvFile(dir) {
|
|
|
880
1413
|
return env
|
|
881
1414
|
}
|
|
882
1415
|
|
|
1416
|
+
function syncEnvToWebApp() {
|
|
1417
|
+
// In monorepo, Convex creates .env.local in backend package
|
|
1418
|
+
// Web app needs NEXT_PUBLIC_CONVEX_URL to connect to Convex
|
|
1419
|
+
const backendEnv = loadEnvFile(backendDir)
|
|
1420
|
+
const webEnvPath = resolve(webAppDir, '.env.local')
|
|
1421
|
+
|
|
1422
|
+
if (backendEnv.NEXT_PUBLIC_CONVEX_URL) {
|
|
1423
|
+
const webEnv = loadEnvFile(webAppDir)
|
|
1424
|
+
|
|
1425
|
+
// Only sync if web app doesn't have the URL or it's different
|
|
1426
|
+
if (webEnv.NEXT_PUBLIC_CONVEX_URL !== backendEnv.NEXT_PUBLIC_CONVEX_URL) {
|
|
1427
|
+
let content = ''
|
|
1428
|
+
if (existsSync(webEnvPath)) {
|
|
1429
|
+
content = readFileSync(webEnvPath, 'utf-8')
|
|
1430
|
+
// Remove existing NEXT_PUBLIC_CONVEX_URL line if present
|
|
1431
|
+
content = content.split('\\n').filter(line => !line.startsWith('NEXT_PUBLIC_CONVEX_URL=')).join('\\n')
|
|
1432
|
+
if (content && !content.endsWith('\\n')) content += '\\n'
|
|
1433
|
+
}
|
|
1434
|
+
content += \`NEXT_PUBLIC_CONVEX_URL=\${backendEnv.NEXT_PUBLIC_CONVEX_URL}\\n\`
|
|
1435
|
+
writeFileSync(webEnvPath, content)
|
|
1436
|
+
console.log('\u2713 Synced NEXT_PUBLIC_CONVEX_URL to web app\\n')
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
return backendEnv
|
|
1441
|
+
}
|
|
1442
|
+
|
|
883
1443
|
async function checkAndInstall() {
|
|
884
1444
|
if (!existsSync(resolve(rootDir, 'node_modules'))) {
|
|
885
1445
|
console.log('\u{1F4E6} Installing dependencies...\\n')
|
|
@@ -888,9 +1448,9 @@ async function checkAndInstall() {
|
|
|
888
1448
|
}
|
|
889
1449
|
|
|
890
1450
|
function startDevServers() {
|
|
891
|
-
const
|
|
1451
|
+
${isMonorepo ? "const backendEnv = syncEnvToWebApp()" : "const backendEnv = loadEnvFile(webAppDir)"}
|
|
892
1452
|
|
|
893
|
-
if (!
|
|
1453
|
+
if (!backendEnv.CONVEX_DEPLOYMENT) {
|
|
894
1454
|
console.log('\u26A0\uFE0F Convex not configured. Run: pnpm dev:setup\\n')
|
|
895
1455
|
console.log('Starting Next.js only...\\n')
|
|
896
1456
|
spawn('pnpm', ['${isMonorepo ? "dev:web" : "dev:next"}'], {
|