kofi-stack-template-generator 2.0.16 → 2.0.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/.turbo/turbo-build.log +6 -6
- package/dist/index.js +585 -54
- package/package.json +1 -1
- package/src/templates.generated.ts +18 -10
- 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/monorepo/package.json.hbs +1 -1
- 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
|
@@ -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
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
convexAuthNextjsProxy,
|
|
3
|
+
createRouteMatcher,
|
|
4
|
+
nextjsProxyRedirect,
|
|
5
|
+
} from '@convex-dev/auth/nextjs/server'
|
|
6
|
+
|
|
7
|
+
const isPublicRoute = createRouteMatcher(['/sign-in', '/sign-up'])
|
|
8
|
+
|
|
9
|
+
export default convexAuthNextjsProxy(async (request, { convexAuth }) => {
|
|
10
|
+
const isAuthenticated = await convexAuth.isAuthenticated()
|
|
11
|
+
|
|
12
|
+
// Redirect unauthenticated users to /sign-up
|
|
13
|
+
if (!isPublicRoute(request) && !isAuthenticated) {
|
|
14
|
+
return nextjsProxyRedirect(request, '/sign-up')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Redirect authenticated users from auth pages to / (dashboard)
|
|
18
|
+
if (isPublicRoute(request) && isAuthenticated) {
|
|
19
|
+
return nextjsProxyRedirect(request, '/')
|
|
20
|
+
}
|
|
21
|
+
})
|