@cogito.ai/cli 0.3.1 → 0.3.2
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/README.md +181 -0
- package/dist/templates/web-nextjs/.env.example +4 -0
- package/dist/templates/web-nextjs/.vscode/settings.json +3 -0
- package/dist/templates/web-nextjs/README.md +25 -1
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +4 -3
- package/dist/templates/web-nextjs/apps/docs/content/docs/features/auth.mdx +139 -0
- package/dist/templates/web-nextjs/apps/web/components.json +25 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +28 -0
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +28 -0
- package/dist/templates/web-nextjs/apps/web/middleware.ts +53 -9
- package/dist/templates/web-nextjs/apps/web/package.json +13 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +142 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +151 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +42 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/layout.tsx +22 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +129 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +2 -4
- package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +21 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +76 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +58 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +154 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +20 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +20 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +50 -0
- package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +9 -0
- package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +14 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/__contract__.ts +14 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +86 -0
- package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +1 -0
- package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +12 -0
- package/dist/templates/web-nextjs/apps/web/src/i18n/request.ts +3 -1
- package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +63 -0
- package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +38 -0
- package/dist/templates/web-nextjs/apps/web/src/infra/providers.ts +6 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +6 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +20 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/shadcn-tailwind.css +95 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/tw-animate.css +1 -0
- package/dist/templates/web-nextjs/pnpm-lock.yaml +2327 -17
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useActionState, useEffect } from 'react'
|
|
4
|
+
import { useParams, useRouter } from 'next/navigation'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { useTranslations } from 'next-intl'
|
|
7
|
+
import { useForm } from 'react-hook-form'
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
9
|
+
import { GitBranch } from 'lucide-react'
|
|
10
|
+
import { toast } from 'sonner'
|
|
11
|
+
import { Button } from '@/components/ui/button'
|
|
12
|
+
import { Input } from '@/components/ui/input'
|
|
13
|
+
import {
|
|
14
|
+
Form,
|
|
15
|
+
FormControl,
|
|
16
|
+
FormField,
|
|
17
|
+
FormItem,
|
|
18
|
+
FormLabel,
|
|
19
|
+
FormMessage,
|
|
20
|
+
} from '@/components/ui/form'
|
|
21
|
+
import { signIn, signInWithGithubForLocale } from '@/features/auth'
|
|
22
|
+
import { signInSchema, type SignInInput } from '@/lib/validations/auth'
|
|
23
|
+
import type { ActionResult } from '@/core/types/auth'
|
|
24
|
+
|
|
25
|
+
export default function LoginPage() {
|
|
26
|
+
const t = useTranslations('auth')
|
|
27
|
+
const router = useRouter()
|
|
28
|
+
const routeParams = useParams<{ locale: string }>()
|
|
29
|
+
const locale = routeParams.locale ?? 'en'
|
|
30
|
+
|
|
31
|
+
const form = useForm<SignInInput>({
|
|
32
|
+
resolver: zodResolver(signInSchema),
|
|
33
|
+
defaultValues: { email: '', password: '' },
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const [state, formAction, isPending] = useActionState<ActionResult | null, FormData>(
|
|
37
|
+
signIn,
|
|
38
|
+
null,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (state?.error) {
|
|
43
|
+
toast.error(state.error)
|
|
44
|
+
}
|
|
45
|
+
}, [state])
|
|
46
|
+
|
|
47
|
+
async function handleGithub() {
|
|
48
|
+
const result = await signInWithGithubForLocale(locale)
|
|
49
|
+
if (result.error) {
|
|
50
|
+
toast.error(result.error)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (result.data?.url) {
|
|
54
|
+
router.push(result.data.url)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex min-h-screen items-center justify-center px-4 py-12">
|
|
60
|
+
<div className="w-full max-w-sm space-y-6">
|
|
61
|
+
<div className="space-y-2 text-center">
|
|
62
|
+
<h1 className="text-3xl font-bold">{t('loginTitle')}</h1>
|
|
63
|
+
<p className="text-muted-foreground">{t('loginSubtitle')}</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<Button
|
|
67
|
+
type="button"
|
|
68
|
+
variant="outline"
|
|
69
|
+
className="w-full"
|
|
70
|
+
onClick={handleGithub}
|
|
71
|
+
>
|
|
72
|
+
<GitBranch className="mr-2 h-4 w-4" />
|
|
73
|
+
{t('githubButton')}
|
|
74
|
+
</Button>
|
|
75
|
+
|
|
76
|
+
<div className="relative">
|
|
77
|
+
<div className="absolute inset-0 flex items-center">
|
|
78
|
+
<span className="w-full border-t" />
|
|
79
|
+
</div>
|
|
80
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
81
|
+
<span className="bg-background px-2 text-muted-foreground">{t('orContinueWith')}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<Form {...form}>
|
|
86
|
+
<form action={formAction} className="space-y-4">
|
|
87
|
+
<input type="hidden" name="locale" value={locale} />
|
|
88
|
+
<FormField
|
|
89
|
+
control={form.control}
|
|
90
|
+
name="email"
|
|
91
|
+
render={({ field }) => (
|
|
92
|
+
<FormItem>
|
|
93
|
+
<FormLabel>{t('emailLabel')}</FormLabel>
|
|
94
|
+
<FormControl>
|
|
95
|
+
<Input
|
|
96
|
+
type="email"
|
|
97
|
+
placeholder={t('emailPlaceholder')}
|
|
98
|
+
{...field}
|
|
99
|
+
/>
|
|
100
|
+
</FormControl>
|
|
101
|
+
<FormMessage />
|
|
102
|
+
</FormItem>
|
|
103
|
+
)}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<FormField
|
|
107
|
+
control={form.control}
|
|
108
|
+
name="password"
|
|
109
|
+
render={({ field }) => (
|
|
110
|
+
<FormItem>
|
|
111
|
+
<FormLabel>{t('passwordLabel')}</FormLabel>
|
|
112
|
+
<FormControl>
|
|
113
|
+
<Input
|
|
114
|
+
type="password"
|
|
115
|
+
placeholder={t('passwordPlaceholder')}
|
|
116
|
+
{...field}
|
|
117
|
+
/>
|
|
118
|
+
</FormControl>
|
|
119
|
+
<FormMessage />
|
|
120
|
+
</FormItem>
|
|
121
|
+
)}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<Button type="submit" className="w-full" disabled={isPending}>
|
|
125
|
+
{isPending ? '...' : t('signInButton')}
|
|
126
|
+
</Button>
|
|
127
|
+
</form>
|
|
128
|
+
</Form>
|
|
129
|
+
|
|
130
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
131
|
+
{t('noAccountText')}{' '}
|
|
132
|
+
<Link
|
|
133
|
+
href={`/${locale}/signup`}
|
|
134
|
+
className="underline underline-offset-4 hover:text-primary"
|
|
135
|
+
>
|
|
136
|
+
{t('signUpLink')}
|
|
137
|
+
</Link>
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useActionState, useEffect, useState } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { useParams } from 'next/navigation'
|
|
6
|
+
import { useTranslations } from 'next-intl'
|
|
7
|
+
import { useForm } from 'react-hook-form'
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
9
|
+
import { toast } from 'sonner'
|
|
10
|
+
import { Button } from '@/components/ui/button'
|
|
11
|
+
import { Input } from '@/components/ui/input'
|
|
12
|
+
import {
|
|
13
|
+
Form,
|
|
14
|
+
FormControl,
|
|
15
|
+
FormDescription,
|
|
16
|
+
FormField,
|
|
17
|
+
FormItem,
|
|
18
|
+
FormLabel,
|
|
19
|
+
FormMessage,
|
|
20
|
+
} from '@/components/ui/form'
|
|
21
|
+
import { signUp } from '@/features/auth'
|
|
22
|
+
import { signUpSchema, type SignUpInput } from '@/lib/validations/auth'
|
|
23
|
+
import type { ActionResult } from '@/core/types/auth'
|
|
24
|
+
import type { SignUpSuccessData } from '@/features/auth/__contract__'
|
|
25
|
+
|
|
26
|
+
export default function SignupPage() {
|
|
27
|
+
const t = useTranslations('auth')
|
|
28
|
+
const routeParams = useParams<{ locale: string }>()
|
|
29
|
+
const locale = routeParams.locale ?? 'en'
|
|
30
|
+
const [verifyEmail, setVerifyEmail] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const form = useForm<SignUpInput>({
|
|
33
|
+
resolver: zodResolver(signUpSchema),
|
|
34
|
+
defaultValues: { email: '', password: '', confirmPassword: '' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const [state, formAction, isPending] = useActionState<ActionResult<SignUpSuccessData> | null, FormData>(
|
|
38
|
+
signUp,
|
|
39
|
+
null,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (state?.error) {
|
|
44
|
+
toast.error(state.error)
|
|
45
|
+
}
|
|
46
|
+
if (state?.data?.success) {
|
|
47
|
+
setVerifyEmail(state.data.email)
|
|
48
|
+
}
|
|
49
|
+
}, [state])
|
|
50
|
+
|
|
51
|
+
if (verifyEmail) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex min-h-screen items-center justify-center px-4 py-12">
|
|
54
|
+
<div className="w-full max-w-sm space-y-4 text-center">
|
|
55
|
+
<h1 className="text-2xl font-bold">{t('verifyEmailTitle')}</h1>
|
|
56
|
+
<p className="text-muted-foreground">
|
|
57
|
+
{t('verifyEmailMessage', { email: verifyEmail })}
|
|
58
|
+
</p>
|
|
59
|
+
<Link href={`/${locale}/login`} className="text-sm underline underline-offset-4">
|
|
60
|
+
{t('signInLink')}
|
|
61
|
+
</Link>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex min-h-screen items-center justify-center px-4 py-12">
|
|
69
|
+
<div className="w-full max-w-sm space-y-6">
|
|
70
|
+
<div className="space-y-2 text-center">
|
|
71
|
+
<h1 className="text-3xl font-bold">{t('signupTitle')}</h1>
|
|
72
|
+
<p className="text-muted-foreground">{t('signupSubtitle')}</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<Form {...form}>
|
|
76
|
+
<form action={formAction} className="space-y-4">
|
|
77
|
+
<input type="hidden" name="locale" value={locale} />
|
|
78
|
+
<FormField
|
|
79
|
+
control={form.control}
|
|
80
|
+
name="email"
|
|
81
|
+
render={({ field }) => (
|
|
82
|
+
<FormItem>
|
|
83
|
+
<FormLabel>{t('emailLabel')}</FormLabel>
|
|
84
|
+
<FormControl>
|
|
85
|
+
<Input
|
|
86
|
+
type="email"
|
|
87
|
+
placeholder={t('emailPlaceholder')}
|
|
88
|
+
{...field}
|
|
89
|
+
/>
|
|
90
|
+
</FormControl>
|
|
91
|
+
<FormDescription>{t('emailHelperText')}</FormDescription>
|
|
92
|
+
<FormMessage />
|
|
93
|
+
</FormItem>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
<FormField
|
|
98
|
+
control={form.control}
|
|
99
|
+
name="password"
|
|
100
|
+
render={({ field }) => (
|
|
101
|
+
<FormItem>
|
|
102
|
+
<FormLabel>{t('passwordLabel')}</FormLabel>
|
|
103
|
+
<FormControl>
|
|
104
|
+
<Input
|
|
105
|
+
type="password"
|
|
106
|
+
placeholder={t('passwordPlaceholder')}
|
|
107
|
+
{...field}
|
|
108
|
+
/>
|
|
109
|
+
</FormControl>
|
|
110
|
+
<FormMessage />
|
|
111
|
+
</FormItem>
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<FormField
|
|
116
|
+
control={form.control}
|
|
117
|
+
name="confirmPassword"
|
|
118
|
+
render={({ field }) => (
|
|
119
|
+
<FormItem>
|
|
120
|
+
<FormLabel>{t('confirmPasswordLabel')}</FormLabel>
|
|
121
|
+
<FormControl>
|
|
122
|
+
<Input
|
|
123
|
+
type="password"
|
|
124
|
+
placeholder={t('confirmPasswordPlaceholder')}
|
|
125
|
+
{...field}
|
|
126
|
+
/>
|
|
127
|
+
</FormControl>
|
|
128
|
+
<FormMessage />
|
|
129
|
+
</FormItem>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
<Button type="submit" className="w-full" disabled={isPending}>
|
|
134
|
+
{isPending ? '...' : t('signUpButton')}
|
|
135
|
+
</Button>
|
|
136
|
+
</form>
|
|
137
|
+
</Form>
|
|
138
|
+
|
|
139
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
140
|
+
{t('hasAccountText')}{' '}
|
|
141
|
+
<Link
|
|
142
|
+
href={`/${locale}/login`}
|
|
143
|
+
className="underline underline-offset-4 hover:text-primary"
|
|
144
|
+
>
|
|
145
|
+
{t('signInLink')}
|
|
146
|
+
</Link>
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl'
|
|
2
|
+
import { getServerClient } from '@/infra/db/client'
|
|
3
|
+
import { signOut } from '@/features/auth'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
|
|
6
|
+
export default async function DashboardPage({
|
|
7
|
+
params,
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ locale: string }>
|
|
10
|
+
}) {
|
|
11
|
+
const { locale } = await params
|
|
12
|
+
const t = useTranslations('auth')
|
|
13
|
+
const supabase = await getServerClient()
|
|
14
|
+
const {
|
|
15
|
+
data: { user },
|
|
16
|
+
} = await supabase.auth.getUser()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-background">
|
|
20
|
+
<header className="border-b">
|
|
21
|
+
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
|
22
|
+
<h1 className="text-xl font-semibold">{t('dashboardTitle')}</h1>
|
|
23
|
+
<div className="flex items-center gap-4">
|
|
24
|
+
<span className="text-sm text-muted-foreground">{user?.email}</span>
|
|
25
|
+
<form action={signOut}>
|
|
26
|
+
<input type="hidden" name="locale" value={locale} />
|
|
27
|
+
<Button type="submit" variant="outline" size="sm">
|
|
28
|
+
{t('signOutButton')}
|
|
29
|
+
</Button>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<main className="mx-auto max-w-7xl px-4 py-8">
|
|
36
|
+
<p className="text-muted-foreground">
|
|
37
|
+
{t('dashboardWelcome', { email: user?.email ?? '' })}
|
|
38
|
+
</p>
|
|
39
|
+
</main>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation'
|
|
2
|
+
import { getServerClient } from '@/infra/db/client'
|
|
3
|
+
|
|
4
|
+
export default async function ProtectedLayout({
|
|
5
|
+
children,
|
|
6
|
+
params,
|
|
7
|
+
}: {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
params: Promise<{ locale: string }>
|
|
10
|
+
}) {
|
|
11
|
+
const { locale } = await params
|
|
12
|
+
const supabase = await getServerClient()
|
|
13
|
+
const {
|
|
14
|
+
data: { user },
|
|
15
|
+
} = await supabase.auth.getUser()
|
|
16
|
+
|
|
17
|
+
if (!user) {
|
|
18
|
+
redirect(`/${locale}/login`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <>{children}</>
|
|
22
|
+
}
|
|
@@ -1,3 +1,129 @@
|
|
|
1
|
-
@
|
|
2
|
-
@tailwind
|
|
3
|
-
@
|
|
1
|
+
@import "../../styles/tw-animate.css";
|
|
2
|
+
@import "../../styles/shadcn-tailwind.css";
|
|
3
|
+
@import "tailwindcss";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--font-heading: var(--font-sans);
|
|
9
|
+
--font-sans: var(--font-sans);
|
|
10
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
11
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
12
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
13
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
14
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
15
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
16
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
17
|
+
--color-sidebar: var(--sidebar);
|
|
18
|
+
--color-chart-5: var(--chart-5);
|
|
19
|
+
--color-chart-4: var(--chart-4);
|
|
20
|
+
--color-chart-3: var(--chart-3);
|
|
21
|
+
--color-chart-2: var(--chart-2);
|
|
22
|
+
--color-chart-1: var(--chart-1);
|
|
23
|
+
--color-ring: var(--ring);
|
|
24
|
+
--color-input: var(--input);
|
|
25
|
+
--color-border: var(--border);
|
|
26
|
+
--color-destructive: var(--destructive);
|
|
27
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
28
|
+
--color-accent: var(--accent);
|
|
29
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
30
|
+
--color-muted: var(--muted);
|
|
31
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
32
|
+
--color-secondary: var(--secondary);
|
|
33
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
34
|
+
--color-primary: var(--primary);
|
|
35
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
36
|
+
--color-popover: var(--popover);
|
|
37
|
+
--color-card-foreground: var(--card-foreground);
|
|
38
|
+
--color-card: var(--card);
|
|
39
|
+
--color-foreground: var(--foreground);
|
|
40
|
+
--color-background: var(--background);
|
|
41
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
42
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
43
|
+
--radius-lg: var(--radius);
|
|
44
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
45
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
46
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
47
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:root {
|
|
51
|
+
--background: oklch(1 0 0);
|
|
52
|
+
--foreground: oklch(0.145 0 0);
|
|
53
|
+
--card: oklch(1 0 0);
|
|
54
|
+
--card-foreground: oklch(0.145 0 0);
|
|
55
|
+
--popover: oklch(1 0 0);
|
|
56
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
57
|
+
--primary: oklch(0.205 0 0);
|
|
58
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
59
|
+
--secondary: oklch(0.97 0 0);
|
|
60
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
61
|
+
--muted: oklch(0.97 0 0);
|
|
62
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
63
|
+
--accent: oklch(0.97 0 0);
|
|
64
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
65
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
66
|
+
--border: oklch(0.922 0 0);
|
|
67
|
+
--input: oklch(0.922 0 0);
|
|
68
|
+
--ring: oklch(0.708 0 0);
|
|
69
|
+
--chart-1: oklch(0.87 0 0);
|
|
70
|
+
--chart-2: oklch(0.556 0 0);
|
|
71
|
+
--chart-3: oklch(0.439 0 0);
|
|
72
|
+
--chart-4: oklch(0.371 0 0);
|
|
73
|
+
--chart-5: oklch(0.269 0 0);
|
|
74
|
+
--radius: 0.625rem;
|
|
75
|
+
--sidebar: oklch(0.985 0 0);
|
|
76
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
77
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
78
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
79
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
80
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
81
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
82
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.dark {
|
|
86
|
+
--background: oklch(0.145 0 0);
|
|
87
|
+
--foreground: oklch(0.985 0 0);
|
|
88
|
+
--card: oklch(0.205 0 0);
|
|
89
|
+
--card-foreground: oklch(0.985 0 0);
|
|
90
|
+
--popover: oklch(0.205 0 0);
|
|
91
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
92
|
+
--primary: oklch(0.922 0 0);
|
|
93
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
94
|
+
--secondary: oklch(0.269 0 0);
|
|
95
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
96
|
+
--muted: oklch(0.269 0 0);
|
|
97
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
98
|
+
--accent: oklch(0.269 0 0);
|
|
99
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
100
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
101
|
+
--border: oklch(1 0 0 / 10%);
|
|
102
|
+
--input: oklch(1 0 0 / 15%);
|
|
103
|
+
--ring: oklch(0.556 0 0);
|
|
104
|
+
--chart-1: oklch(0.87 0 0);
|
|
105
|
+
--chart-2: oklch(0.556 0 0);
|
|
106
|
+
--chart-3: oklch(0.439 0 0);
|
|
107
|
+
--chart-4: oklch(0.371 0 0);
|
|
108
|
+
--chart-5: oklch(0.269 0 0);
|
|
109
|
+
--sidebar: oklch(0.205 0 0);
|
|
110
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
111
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
112
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
113
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
114
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
115
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
116
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@layer base {
|
|
120
|
+
* {
|
|
121
|
+
@apply border-border outline-ring/50;
|
|
122
|
+
}
|
|
123
|
+
body {
|
|
124
|
+
@apply bg-background text-foreground;
|
|
125
|
+
}
|
|
126
|
+
html {
|
|
127
|
+
@apply font-sans;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -2,11 +2,9 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import { NextIntlClientProvider } from 'next-intl'
|
|
3
3
|
import { getMessages } from 'next-intl/server'
|
|
4
4
|
import { notFound } from 'next/navigation'
|
|
5
|
+
import { isLocale } from '@/i18n/config'
|
|
5
6
|
import './globals.css'
|
|
6
7
|
|
|
7
|
-
const locales = ['en', 'zh'] as const
|
|
8
|
-
type Locale = (typeof locales)[number]
|
|
9
|
-
|
|
10
8
|
export const metadata: Metadata = {
|
|
11
9
|
title: 'AgentDock Web Template',
|
|
12
10
|
description: 'Generated by AgentDock scaffold platform',
|
|
@@ -21,7 +19,7 @@ export default async function LocaleLayout({
|
|
|
21
19
|
}) {
|
|
22
20
|
const { locale } = await params
|
|
23
21
|
|
|
24
|
-
if (!
|
|
22
|
+
if (!isLocale(locale)) {
|
|
25
23
|
notFound()
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { getServerClient } from '@/infra/db/client'
|
|
3
|
+
import { defaultLocale } from '@/i18n/config'
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
const { searchParams, origin } = new URL(request.url)
|
|
7
|
+
const code = searchParams.get('code')
|
|
8
|
+
const nextParam = searchParams.get('next')
|
|
9
|
+
const next = nextParam && nextParam.startsWith('/') ? nextParam : `/${defaultLocale}/dashboard`
|
|
10
|
+
|
|
11
|
+
if (code) {
|
|
12
|
+
const supabase = await getServerClient()
|
|
13
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
|
14
|
+
|
|
15
|
+
if (!error) {
|
|
16
|
+
return NextResponse.redirect(`${origin}${next}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return NextResponse.redirect(`${origin}/${defaultLocale}/login?error=auth_callback_error`)
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-card text-card-foreground",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
function Alert({
|
|
23
|
+
className,
|
|
24
|
+
variant,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
data-slot="alert"
|
|
30
|
+
role="alert"
|
|
31
|
+
className={cn(alertVariants({ variant }), className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
data-slot="alert-title"
|
|
41
|
+
className={cn(
|
|
42
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AlertDescription({
|
|
51
|
+
className,
|
|
52
|
+
...props
|
|
53
|
+
}: React.ComponentProps<"div">) {
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
data-slot="alert-description"
|
|
57
|
+
className={cn(
|
|
58
|
+
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
data-slot="alert-action"
|
|
70
|
+
className={cn("absolute top-2 right-2", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { Alert, AlertTitle, AlertDescription, AlertAction }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
12
|
+
outline:
|
|
13
|
+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
14
|
+
secondary:
|
|
15
|
+
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
16
|
+
ghost:
|
|
17
|
+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
18
|
+
destructive:
|
|
19
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default:
|
|
24
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
25
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
28
|
+
icon: "size-8",
|
|
29
|
+
"icon-xs":
|
|
30
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
"icon-sm":
|
|
32
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
33
|
+
"icon-lg": "size-9",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: "default",
|
|
38
|
+
size: "default",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
function Button({
|
|
44
|
+
className,
|
|
45
|
+
variant = "default",
|
|
46
|
+
size = "default",
|
|
47
|
+
...props
|
|
48
|
+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
49
|
+
return (
|
|
50
|
+
<ButtonPrimitive
|
|
51
|
+
data-slot="button"
|
|
52
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { Button, buttonVariants }
|