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
|
@@ -1,61 +1,115 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useQuery } from 'convex/react'
|
|
4
|
+
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../convex/_generated/api{{/if}}'
|
|
5
|
+
import { DashboardLayout } from '@/components/dashboard/dashboard-layout'
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
7
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
4
8
|
|
|
5
9
|
export default function HomePage() {
|
|
6
|
-
const
|
|
10
|
+
const user = useQuery(api.users.viewer)
|
|
7
11
|
|
|
8
|
-
if (
|
|
12
|
+
if (user === undefined) {
|
|
9
13
|
return (
|
|
10
|
-
<
|
|
11
|
-
<div className="
|
|
12
|
-
|
|
14
|
+
<DashboardLayout title="Dashboard">
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<Skeleton className="h-8 w-64" />
|
|
17
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
18
|
+
<Skeleton className="h-32" />
|
|
19
|
+
<Skeleton className="h-32" />
|
|
20
|
+
<Skeleton className="h-32" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</DashboardLayout>
|
|
13
24
|
)
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
return (
|
|
17
|
-
<
|
|
18
|
-
<div className="
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
<DashboardLayout title="Dashboard">
|
|
29
|
+
<div className="space-y-6">
|
|
30
|
+
<div>
|
|
31
|
+
<h1 className="text-3xl font-bold tracking-tight">
|
|
32
|
+
Welcome back{user?.name ? `, ${user.name}` : ''}!
|
|
33
|
+
</h1>
|
|
34
|
+
<p className="text-muted-foreground">
|
|
35
|
+
Here's what's happening with your project today.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
className="
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
40
|
+
<Card>
|
|
41
|
+
<CardHeader>
|
|
42
|
+
<CardTitle>Getting Started</CardTitle>
|
|
43
|
+
<CardDescription>Quick start guide for your app</CardDescription>
|
|
44
|
+
</CardHeader>
|
|
45
|
+
<CardContent>
|
|
46
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
47
|
+
<li>Customize your dashboard layout</li>
|
|
48
|
+
<li>Add new pages to the sidebar</li>
|
|
49
|
+
<li>Connect your data sources</li>
|
|
50
|
+
</ul>
|
|
51
|
+
</CardContent>
|
|
52
|
+
</Card>
|
|
53
|
+
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader>
|
|
56
|
+
<CardTitle>Documentation</CardTitle>
|
|
57
|
+
<CardDescription>Learn how to build with Kofi Stack</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
61
|
+
<li>
|
|
62
|
+
<a
|
|
63
|
+
href="https://docs.convex.dev"
|
|
64
|
+
target="_blank"
|
|
65
|
+
rel="noopener noreferrer"
|
|
66
|
+
className="text-primary hover:underline"
|
|
67
|
+
>
|
|
68
|
+
Convex Documentation
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
<li>
|
|
72
|
+
<a
|
|
73
|
+
href="https://ui.shadcn.com"
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
className="text-primary hover:underline"
|
|
77
|
+
>
|
|
78
|
+
shadcn/ui Components
|
|
79
|
+
</a>
|
|
80
|
+
</li>
|
|
81
|
+
<li>
|
|
82
|
+
<a
|
|
83
|
+
href="https://nextjs.org/docs"
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
className="text-primary hover:underline"
|
|
87
|
+
>
|
|
88
|
+
Next.js Documentation
|
|
89
|
+
</a>
|
|
90
|
+
</li>
|
|
91
|
+
</ul>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
|
|
95
|
+
<Card>
|
|
96
|
+
<CardHeader>
|
|
97
|
+
<CardTitle>Your Stack</CardTitle>
|
|
98
|
+
<CardDescription>Technologies powering your app</CardDescription>
|
|
99
|
+
</CardHeader>
|
|
100
|
+
<CardContent>
|
|
101
|
+
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
102
|
+
<li>Next.js 15 with App Router</li>
|
|
103
|
+
<li>Convex for backend & database</li>
|
|
104
|
+
<li>Convex Auth for authentication</li>
|
|
105
|
+
<li>shadcn/ui components</li>
|
|
106
|
+
<li>Tailwind CSS for styling</li>
|
|
107
|
+
</ul>
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
</div>
|
|
57
111
|
|
|
58
|
-
<div className="pt-
|
|
112
|
+
<div className="pt-4 border-t">
|
|
59
113
|
<p className="text-sm text-muted-foreground">
|
|
60
114
|
Created with{' '}
|
|
61
115
|
<a
|
|
@@ -69,6 +123,6 @@ export default function HomePage() {
|
|
|
69
123
|
</p>
|
|
70
124
|
</div>
|
|
71
125
|
</div>
|
|
72
|
-
</
|
|
126
|
+
</DashboardLayout>
|
|
73
127
|
)
|
|
74
128
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
|
+
import { Input } from '@/components/ui/input'
|
|
10
|
+
import { Label } from '@/components/ui/label'
|
|
11
|
+
import { Separator } from '@/components/ui/separator'
|
|
12
|
+
|
|
13
|
+
export function SignInForm() {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const { signIn } = useAuthActions()
|
|
16
|
+
const [email, setEmail] = useState('')
|
|
17
|
+
const [password, setPassword] = useState('')
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
setIsLoading(true)
|
|
24
|
+
setError(null)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const formData = new FormData()
|
|
28
|
+
formData.append('email', email)
|
|
29
|
+
formData.append('password', password)
|
|
30
|
+
formData.append('flow', 'signIn')
|
|
31
|
+
|
|
32
|
+
await signIn('password', formData)
|
|
33
|
+
router.push('/')
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError('Invalid email or password')
|
|
36
|
+
} finally {
|
|
37
|
+
setIsLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
42
|
+
void signIn(provider)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader className="text-center">
|
|
48
|
+
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
|
49
|
+
<CardDescription>Sign in to your account to continue</CardDescription>
|
|
50
|
+
</CardHeader>
|
|
51
|
+
<CardContent>
|
|
52
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
53
|
+
<div className="space-y-2">
|
|
54
|
+
<Label htmlFor="email">Email</Label>
|
|
55
|
+
<Input
|
|
56
|
+
id="email"
|
|
57
|
+
type="email"
|
|
58
|
+
placeholder="you@example.com"
|
|
59
|
+
value={email}
|
|
60
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
61
|
+
required
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<Label htmlFor="password">Password</Label>
|
|
66
|
+
<Input
|
|
67
|
+
id="password"
|
|
68
|
+
type="password"
|
|
69
|
+
placeholder="Enter your password"
|
|
70
|
+
value={password}
|
|
71
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
72
|
+
required
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{error && (
|
|
77
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
81
|
+
{isLoading ? 'Signing in...' : 'Sign In'}
|
|
82
|
+
</Button>
|
|
83
|
+
</form>
|
|
84
|
+
|
|
85
|
+
<div className="relative my-6">
|
|
86
|
+
<div className="absolute inset-0 flex items-center">
|
|
87
|
+
<Separator className="w-full" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
90
|
+
<span className="bg-card px-2 text-muted-foreground">Or continue with</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div className="grid grid-cols-2 gap-4">
|
|
95
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('github')}>
|
|
96
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
97
|
+
<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"/>
|
|
98
|
+
</svg>
|
|
99
|
+
GitHub
|
|
100
|
+
</Button>
|
|
101
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('google')}>
|
|
102
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
103
|
+
<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"/>
|
|
104
|
+
<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"/>
|
|
105
|
+
<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"/>
|
|
106
|
+
<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"/>
|
|
107
|
+
</svg>
|
|
108
|
+
Google
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
113
|
+
Don't have an account?{' '}
|
|
114
|
+
<Link href="/sign-up" className="text-primary hover:underline font-medium">
|
|
115
|
+
Sign up
|
|
116
|
+
</Link>
|
|
117
|
+
</p>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
|
+
import { Input } from '@/components/ui/input'
|
|
10
|
+
import { Label } from '@/components/ui/label'
|
|
11
|
+
import { Separator } from '@/components/ui/separator'
|
|
12
|
+
|
|
13
|
+
export function SignUpForm() {
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const { signIn } = useAuthActions()
|
|
16
|
+
const [name, setName] = useState('')
|
|
17
|
+
const [email, setEmail] = useState('')
|
|
18
|
+
const [password, setPassword] = useState('')
|
|
19
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
20
|
+
const [error, setError] = useState<string | null>(null)
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
setIsLoading(true)
|
|
25
|
+
setError(null)
|
|
26
|
+
|
|
27
|
+
if (password.length < 8) {
|
|
28
|
+
setError('Password must be at least 8 characters')
|
|
29
|
+
setIsLoading(false)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const formData = new FormData()
|
|
35
|
+
formData.append('name', name)
|
|
36
|
+
formData.append('email', email)
|
|
37
|
+
formData.append('password', password)
|
|
38
|
+
formData.append('flow', 'signUp')
|
|
39
|
+
|
|
40
|
+
await signIn('password', formData)
|
|
41
|
+
router.push('/')
|
|
42
|
+
} catch (err) {
|
|
43
|
+
setError('Failed to create account. Email may already be in use.')
|
|
44
|
+
} finally {
|
|
45
|
+
setIsLoading(false)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleSocialSignIn = (provider: 'github' | 'google') => {
|
|
50
|
+
void signIn(provider)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Card>
|
|
55
|
+
<CardHeader className="text-center">
|
|
56
|
+
<CardTitle className="text-2xl">Create an account</CardTitle>
|
|
57
|
+
<CardDescription>Enter your details to get started</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
61
|
+
<div className="space-y-2">
|
|
62
|
+
<Label htmlFor="name">Name</Label>
|
|
63
|
+
<Input
|
|
64
|
+
id="name"
|
|
65
|
+
type="text"
|
|
66
|
+
placeholder="Your name"
|
|
67
|
+
value={name}
|
|
68
|
+
onChange={(e) => setName(e.target.value)}
|
|
69
|
+
required
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<Label htmlFor="email">Email</Label>
|
|
74
|
+
<Input
|
|
75
|
+
id="email"
|
|
76
|
+
type="email"
|
|
77
|
+
placeholder="you@example.com"
|
|
78
|
+
value={email}
|
|
79
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
80
|
+
required
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="space-y-2">
|
|
84
|
+
<Label htmlFor="password">Password</Label>
|
|
85
|
+
<Input
|
|
86
|
+
id="password"
|
|
87
|
+
type="password"
|
|
88
|
+
placeholder="At least 8 characters"
|
|
89
|
+
value={password}
|
|
90
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
91
|
+
required
|
|
92
|
+
minLength={8}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{error && (
|
|
97
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
101
|
+
{isLoading ? 'Creating account...' : 'Create Account'}
|
|
102
|
+
</Button>
|
|
103
|
+
</form>
|
|
104
|
+
|
|
105
|
+
<div className="relative my-6">
|
|
106
|
+
<div className="absolute inset-0 flex items-center">
|
|
107
|
+
<Separator className="w-full" />
|
|
108
|
+
</div>
|
|
109
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
110
|
+
<span className="bg-card px-2 text-muted-foreground">Or continue with</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="grid grid-cols-2 gap-4">
|
|
115
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('github')}>
|
|
116
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
117
|
+
<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"/>
|
|
118
|
+
</svg>
|
|
119
|
+
GitHub
|
|
120
|
+
</Button>
|
|
121
|
+
<Button variant="outline" onClick={() => handleSocialSignIn('google')}>
|
|
122
|
+
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
|
|
123
|
+
<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"/>
|
|
124
|
+
<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"/>
|
|
125
|
+
<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"/>
|
|
126
|
+
<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"/>
|
|
127
|
+
</svg>
|
|
128
|
+
Google
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<p className="mt-6 text-center text-sm text-muted-foreground">
|
|
133
|
+
Already have an account?{' '}
|
|
134
|
+
<Link href="/sign-in" className="text-primary hover:underline font-medium">
|
|
135
|
+
Sign in
|
|
136
|
+
</Link>
|
|
137
|
+
</p>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AudioWaveform,
|
|
5
|
+
Command,
|
|
6
|
+
GalleryVerticalEnd,
|
|
7
|
+
Home,
|
|
8
|
+
Settings,
|
|
9
|
+
ChevronsUpDown,
|
|
10
|
+
LogOut,
|
|
11
|
+
User,
|
|
12
|
+
} from 'lucide-react'
|
|
13
|
+
import { useRouter } from 'next/navigation'
|
|
14
|
+
import { useAuthActions } from '@convex-dev/auth/react'
|
|
15
|
+
import { useQuery } from 'convex/react'
|
|
16
|
+
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
17
|
+
import {
|
|
18
|
+
Sidebar,
|
|
19
|
+
SidebarContent,
|
|
20
|
+
SidebarFooter,
|
|
21
|
+
SidebarGroup,
|
|
22
|
+
SidebarGroupContent,
|
|
23
|
+
SidebarGroupLabel,
|
|
24
|
+
SidebarHeader,
|
|
25
|
+
SidebarMenu,
|
|
26
|
+
SidebarMenuButton,
|
|
27
|
+
SidebarMenuItem,
|
|
28
|
+
} from '@/components/ui/sidebar'
|
|
29
|
+
import {
|
|
30
|
+
DropdownMenu,
|
|
31
|
+
DropdownMenuContent,
|
|
32
|
+
DropdownMenuItem,
|
|
33
|
+
DropdownMenuSeparator,
|
|
34
|
+
DropdownMenuTrigger,
|
|
35
|
+
} from '@/components/ui/dropdown-menu'
|
|
36
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
37
|
+
|
|
38
|
+
const navigation = [
|
|
39
|
+
{
|
|
40
|
+
title: 'Home',
|
|
41
|
+
url: '/',
|
|
42
|
+
icon: Home,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Settings',
|
|
46
|
+
url: '/settings',
|
|
47
|
+
icon: Settings,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export function AppSidebar() {
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
const { signOut } = useAuthActions()
|
|
54
|
+
const user = useQuery(api.users.viewer)
|
|
55
|
+
|
|
56
|
+
const handleSignOut = async () => {
|
|
57
|
+
await signOut()
|
|
58
|
+
router.push('/sign-in')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getInitials = (name?: string | null) => {
|
|
62
|
+
if (!name) return 'U'
|
|
63
|
+
return name
|
|
64
|
+
.split(' ')
|
|
65
|
+
.map((n) => n[0])
|
|
66
|
+
.join('')
|
|
67
|
+
.toUpperCase()
|
|
68
|
+
.slice(0, 2)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Sidebar>
|
|
73
|
+
<SidebarHeader>
|
|
74
|
+
<SidebarMenu>
|
|
75
|
+
<SidebarMenuItem>
|
|
76
|
+
<SidebarMenuButton size="lg" asChild>
|
|
77
|
+
<a href="/">
|
|
78
|
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
79
|
+
<GalleryVerticalEnd className="size-4" />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex flex-col gap-0.5 leading-none">
|
|
82
|
+
<span className="font-semibold">{{projectName}}</span>
|
|
83
|
+
<span className="text-xs text-muted-foreground">Dashboard</span>
|
|
84
|
+
</div>
|
|
85
|
+
</a>
|
|
86
|
+
</SidebarMenuButton>
|
|
87
|
+
</SidebarMenuItem>
|
|
88
|
+
</SidebarMenu>
|
|
89
|
+
</SidebarHeader>
|
|
90
|
+
<SidebarContent>
|
|
91
|
+
<SidebarGroup>
|
|
92
|
+
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
93
|
+
<SidebarGroupContent>
|
|
94
|
+
<SidebarMenu>
|
|
95
|
+
{navigation.map((item) => (
|
|
96
|
+
<SidebarMenuItem key={item.title}>
|
|
97
|
+
<SidebarMenuButton asChild>
|
|
98
|
+
<a href={item.url}>
|
|
99
|
+
<item.icon className="size-4" />
|
|
100
|
+
<span>{item.title}</span>
|
|
101
|
+
</a>
|
|
102
|
+
</SidebarMenuButton>
|
|
103
|
+
</SidebarMenuItem>
|
|
104
|
+
))}
|
|
105
|
+
</SidebarMenu>
|
|
106
|
+
</SidebarGroupContent>
|
|
107
|
+
</SidebarGroup>
|
|
108
|
+
</SidebarContent>
|
|
109
|
+
<SidebarFooter>
|
|
110
|
+
<SidebarMenu>
|
|
111
|
+
<SidebarMenuItem>
|
|
112
|
+
<DropdownMenu>
|
|
113
|
+
<DropdownMenuTrigger asChild>
|
|
114
|
+
<SidebarMenuButton
|
|
115
|
+
size="lg"
|
|
116
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
117
|
+
>
|
|
118
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
119
|
+
<AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />
|
|
120
|
+
<AvatarFallback className="rounded-lg">
|
|
121
|
+
{getInitials(user?.name)}
|
|
122
|
+
</AvatarFallback>
|
|
123
|
+
</Avatar>
|
|
124
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
125
|
+
<span className="truncate font-semibold">{user?.name ?? 'User'}</span>
|
|
126
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
127
|
+
{user?.email ?? ''}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<ChevronsUpDown className="ml-auto size-4" />
|
|
131
|
+
</SidebarMenuButton>
|
|
132
|
+
</DropdownMenuTrigger>
|
|
133
|
+
<DropdownMenuContent
|
|
134
|
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
135
|
+
side="bottom"
|
|
136
|
+
align="end"
|
|
137
|
+
sideOffset={4}
|
|
138
|
+
>
|
|
139
|
+
<DropdownMenuItem asChild>
|
|
140
|
+
<a href="/settings">
|
|
141
|
+
<User className="mr-2 size-4" />
|
|
142
|
+
Profile
|
|
143
|
+
</a>
|
|
144
|
+
</DropdownMenuItem>
|
|
145
|
+
<DropdownMenuItem asChild>
|
|
146
|
+
<a href="/settings">
|
|
147
|
+
<Settings className="mr-2 size-4" />
|
|
148
|
+
Settings
|
|
149
|
+
</a>
|
|
150
|
+
</DropdownMenuItem>
|
|
151
|
+
<DropdownMenuSeparator />
|
|
152
|
+
<DropdownMenuItem onClick={handleSignOut}>
|
|
153
|
+
<LogOut className="mr-2 size-4" />
|
|
154
|
+
Sign out
|
|
155
|
+
</DropdownMenuItem>
|
|
156
|
+
</DropdownMenuContent>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
</SidebarMenuItem>
|
|
159
|
+
</SidebarMenu>
|
|
160
|
+
</SidebarFooter>
|
|
161
|
+
</Sidebar>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
|
4
|
+
import { Separator } from '@/components/ui/separator'
|
|
5
|
+
import {
|
|
6
|
+
Breadcrumb,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbList,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
} from '@/components/ui/breadcrumb'
|
|
11
|
+
import { AppSidebar } from './app-sidebar'
|
|
12
|
+
|
|
13
|
+
interface DashboardLayoutProps {
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
title?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {
|
|
19
|
+
return (
|
|
20
|
+
<SidebarProvider>
|
|
21
|
+
<AppSidebar />
|
|
22
|
+
<SidebarInset>
|
|
23
|
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
|
24
|
+
<SidebarTrigger className="-ml-1" />
|
|
25
|
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
26
|
+
<Breadcrumb>
|
|
27
|
+
<BreadcrumbList>
|
|
28
|
+
<BreadcrumbItem>
|
|
29
|
+
<BreadcrumbPage>{title}</BreadcrumbPage>
|
|
30
|
+
</BreadcrumbItem>
|
|
31
|
+
</BreadcrumbList>
|
|
32
|
+
</Breadcrumb>
|
|
33
|
+
</header>
|
|
34
|
+
<main className="flex-1 p-4 md:p-6">{children}</main>
|
|
35
|
+
</SidebarInset>
|
|
36
|
+
</SidebarProvider>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useConvexAuth,
|
|
3
|
+
import { useConvexAuth, useQuery } from 'convex/react'
|
|
4
4
|
import { useAuthActions } from '@convex-dev/auth/react'
|
|
5
|
-
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../../convex/_generated/api{{/if}}'
|
|
5
|
+
import { api } from '{{#if (eq structure 'monorepo')}}@repo/backend/convex/_generated/api{{else}}../../convex/_generated/api{{/if}}'
|
|
6
6
|
|
|
7
7
|
export function useAuth() {
|
|
8
8
|
const { isAuthenticated, isLoading } = useConvexAuth()
|
|
9
9
|
const { signIn, signOut } = useAuthActions()
|
|
10
|
-
const user = useQuery(api.users.
|
|
10
|
+
const user = useQuery(api.users.viewer)
|
|
11
11
|
|
|
12
12
|
return {
|
|
13
13
|
isAuthenticated,
|
|
@@ -16,6 +16,16 @@ export function useAuth() {
|
|
|
16
16
|
signIn: (provider: 'github' | 'google') => {
|
|
17
17
|
void signIn(provider)
|
|
18
18
|
},
|
|
19
|
+
signInWithPassword: async (email: string, password: string, flow: 'signIn' | 'signUp' = 'signIn', name?: string) => {
|
|
20
|
+
const formData = new FormData()
|
|
21
|
+
formData.append('email', email)
|
|
22
|
+
formData.append('password', password)
|
|
23
|
+
formData.append('flow', flow)
|
|
24
|
+
if (name) {
|
|
25
|
+
formData.append('name', name)
|
|
26
|
+
}
|
|
27
|
+
await signIn('password', formData)
|
|
28
|
+
},
|
|
19
29
|
signOut: () => {
|
|
20
30
|
void signOut()
|
|
21
31
|
},
|