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.
@@ -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
  },
@@ -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
+ })