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.
@@ -1,61 +1,115 @@
1
1
  'use client'
2
2
 
3
- import { useAuth } from '@/lib/auth'
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 { isAuthenticated, isLoading, signIn, signOut, user } = useAuth()
10
+ const user = useQuery(api.users.viewer)
7
11
 
8
- if (isLoading) {
12
+ if (user === undefined) {
9
13
  return (
10
- <main className="min-h-screen flex items-center justify-center">
11
- <div className="animate-pulse">Loading...</div>
12
- </main>
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
- <main className="min-h-screen flex flex-col items-center justify-center p-8">
18
- <div className="max-w-2xl text-center space-y-8">
19
- <h1 className="text-4xl font-bold">{{projectName}}</h1>
20
- <p className="text-xl text-muted-foreground">
21
- Built with Next.js, Convex, Better-Auth, and shadcn/ui
22
- </p>
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
- {isAuthenticated ? (
25
- <div className="space-y-4">
26
- <p className="text-lg">
27
- Welcome, <span className="font-semibold">{user?.name || user?.email}</span>!
28
- </p>
29
- <button
30
- onClick={() => signOut()}
31
- className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
32
- >
33
- Sign Out
34
- </button>
35
- </div>
36
- ) : (
37
- <div className="space-y-4">
38
- <p className="text-muted-foreground">
39
- Sign in to get started
40
- </p>
41
- <div className="flex gap-4 justify-center">
42
- <button
43
- onClick={() => signIn('github')}
44
- className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition"
45
- >
46
- Sign in with GitHub
47
- </button>
48
- <button
49
- onClick={() => signIn('google')}
50
- className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
51
- >
52
- Sign in with Google
53
- </button>
54
- </div>
55
- </div>
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-8 border-t border-border">
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
- </main>
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&apos;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, useMutation, useQuery } from 'convex/react'
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.current)
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
  },