kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: fix webapp build
rodzic
425ce4793c
commit
71aa41e08d
|
@ -1,4 +1,5 @@
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
import { OAuthSuccessCallback } from './oauth-success-callback'
|
import { OAuthSuccessCallback } from './oauth-success-callback'
|
||||||
|
|
||||||
|
@ -10,5 +11,9 @@ export default async function Page({
|
||||||
const { provider } = await params
|
const { provider } = await params
|
||||||
assert(provider, 'Missing provider')
|
assert(provider, 'Missing provider')
|
||||||
|
|
||||||
return <OAuthSuccessCallback provider={provider} />
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<OAuthSuccessCallback provider={provider} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||||
|
import { isValidEmail, isValidPassword } from '@agentic/platform-validators'
|
||||||
|
import { useForm } from '@tanstack/react-form'
|
||||||
|
import { Loader2Icon } from 'lucide-react'
|
||||||
|
import { redirect, RedirectType } from 'next/navigation'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useNextUrl,
|
||||||
|
useUnauthenticatedAgentic
|
||||||
|
} from '@/components/agentic-provider'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { GitHubIcon } from '@/icons/github'
|
||||||
|
import { toastError } from '@/lib/notifications'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export function LoginForm() {
|
||||||
|
const ctx = useUnauthenticatedAgentic()
|
||||||
|
const nextUrl = useNextUrl()
|
||||||
|
|
||||||
|
const onAuthWithGitHub = useCallback(async () => {
|
||||||
|
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
||||||
|
|
||||||
|
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
||||||
|
|
||||||
|
redirect(url, RedirectType.push)
|
||||||
|
}, [ctx, nextUrl])
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
validators: {
|
||||||
|
onChange: z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.refine((email) => isValidEmail(email)),
|
||||||
|
password: z.string().refine((password) => isValidPassword(password))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
const res = await ctx!.api.signInWithPassword({
|
||||||
|
email: value.email,
|
||||||
|
password: value.password
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('login success', res)
|
||||||
|
} catch (err: any) {
|
||||||
|
void toastError(err, { label: 'Login error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(nextUrl || '/app', RedirectType.push)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
||||||
|
<form
|
||||||
|
className={cn('flex flex-col gap-6 w-full')}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col items-center gap-2 text-center'>
|
||||||
|
<h1 className='text-2xl font-bold'>Login to your account</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<form.Field
|
||||||
|
name='email'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor={field.name}>Email</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='email'
|
||||||
|
required
|
||||||
|
placeholder='Email'
|
||||||
|
autoComplete='email'
|
||||||
|
autoFocus={true}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name='password'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<Label htmlFor={field.name}>Password</Label>
|
||||||
|
|
||||||
|
{/* <a
|
||||||
|
href='/forgot-password'
|
||||||
|
className='ml-auto text-xs underline-offset-4 hover:underline'
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Forgot your password?
|
||||||
|
</a> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='password'
|
||||||
|
required
|
||||||
|
placeholder='Password'
|
||||||
|
// autoFocus={error?.type === 'invalid_password'}
|
||||||
|
autoComplete='current-password'
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => [
|
||||||
|
state.canSubmit,
|
||||||
|
state.isSubmitting,
|
||||||
|
state.isTouched
|
||||||
|
]}
|
||||||
|
children={([canSubmit, isSubmitting, isTouched]) => (
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
disabled={!(isTouched && canSubmit && ctx)}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
||||||
|
|
||||||
|
<span>Login</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||||
|
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||||
|
Or continue with
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-full'
|
||||||
|
onClick={onAuthWithGitHub}
|
||||||
|
>
|
||||||
|
<GitHubIcon />
|
||||||
|
|
||||||
|
<span>Login with GitHub</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-center text-xs'>
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<a
|
||||||
|
href={`/signup?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
||||||
|
className='underline underline-offset-4'
|
||||||
|
>
|
||||||
|
Sign up
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,185 +1,11 @@
|
||||||
'use client'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
import { LoginForm } from './login-form'
|
||||||
import { isValidEmail, isValidPassword } from '@agentic/platform-validators'
|
|
||||||
import { useForm } from '@tanstack/react-form'
|
|
||||||
import { Loader2Icon } from 'lucide-react'
|
|
||||||
import { redirect, RedirectType } from 'next/navigation'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import {
|
|
||||||
useNextUrl,
|
|
||||||
useUnauthenticatedAgentic
|
|
||||||
} from '@/components/agentic-provider'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { GitHubIcon } from '@/icons/github'
|
|
||||||
import { toastError } from '@/lib/notifications'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
const ctx = useUnauthenticatedAgentic()
|
|
||||||
const nextUrl = useNextUrl()
|
|
||||||
|
|
||||||
const onAuthWithGitHub = useCallback(async () => {
|
|
||||||
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
|
||||||
|
|
||||||
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
|
||||||
|
|
||||||
redirect(url, RedirectType.push)
|
|
||||||
}, [ctx, nextUrl])
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
password: ''
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onChange: z.object({
|
|
||||||
email: z
|
|
||||||
.string()
|
|
||||||
.email()
|
|
||||||
.refine((email) => isValidEmail(email)),
|
|
||||||
password: z.string().refine((password) => isValidPassword(password))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
try {
|
|
||||||
const res = await ctx!.api.signInWithPassword({
|
|
||||||
email: value.email,
|
|
||||||
password: value.password
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('login success', res)
|
|
||||||
} catch (err: any) {
|
|
||||||
void toastError(err, { label: 'Login error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(nextUrl || '/app', RedirectType.push)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense>
|
||||||
<section>
|
<LoginForm />
|
||||||
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
</Suspense>
|
||||||
<form
|
|
||||||
className={cn('flex flex-col gap-6 w-full')}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
void form.handleSubmit()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex flex-col items-center gap-2 text-center'>
|
|
||||||
<h1 className='text-2xl font-bold'>Login to your account</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6'>
|
|
||||||
<form.Field
|
|
||||||
name='email'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<Label htmlFor={field.name}>Email</Label>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='email'
|
|
||||||
required
|
|
||||||
placeholder='Email'
|
|
||||||
autoComplete='email'
|
|
||||||
autoFocus={true}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Field
|
|
||||||
name='password'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<Label htmlFor={field.name}>Password</Label>
|
|
||||||
|
|
||||||
{/* <a
|
|
||||||
href='/forgot-password'
|
|
||||||
className='ml-auto text-xs underline-offset-4 hover:underline'
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
Forgot your password?
|
|
||||||
</a> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='password'
|
|
||||||
required
|
|
||||||
placeholder='Password'
|
|
||||||
// autoFocus={error?.type === 'invalid_password'}
|
|
||||||
autoComplete='current-password'
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => [
|
|
||||||
state.canSubmit,
|
|
||||||
state.isSubmitting,
|
|
||||||
state.isTouched
|
|
||||||
]}
|
|
||||||
children={([canSubmit, isSubmitting, isTouched]) => (
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
disabled={!(isTouched && canSubmit && ctx)}
|
|
||||||
className='w-full'
|
|
||||||
>
|
|
||||||
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
|
||||||
|
|
||||||
<span>Login</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
|
||||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
|
||||||
Or continue with
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full'
|
|
||||||
onClick={onAuthWithGitHub}
|
|
||||||
>
|
|
||||||
<GitHubIcon />
|
|
||||||
|
|
||||||
<span>Login with GitHub</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='text-center text-xs'>
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<a
|
|
||||||
href={`/signup?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
|
||||||
className='underline underline-offset-4'
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,230 +1,11 @@
|
||||||
'use client'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
import { sanitizeSearchParams } from '@agentic/platform-core'
|
import { SignupForm } from './signup-form'
|
||||||
import {
|
|
||||||
isValidEmail,
|
|
||||||
isValidPassword,
|
|
||||||
isValidUsername
|
|
||||||
} from '@agentic/platform-validators'
|
|
||||||
import { useForm } from '@tanstack/react-form'
|
|
||||||
import { Loader2Icon } from 'lucide-react'
|
|
||||||
import { redirect, RedirectType } from 'next/navigation'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import {
|
|
||||||
useNextUrl,
|
|
||||||
useUnauthenticatedAgentic
|
|
||||||
} from '@/components/agentic-provider'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { GitHubIcon } from '@/icons/github'
|
|
||||||
import { toastError } from '@/lib/notifications'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export default function SignupPage() {
|
|
||||||
// const [error] = useState<PasswordLoginError | undefined>(undefined)
|
|
||||||
const ctx = useUnauthenticatedAgentic()
|
|
||||||
const nextUrl = useNextUrl()
|
|
||||||
|
|
||||||
const onAuthWithGitHub = useCallback(async () => {
|
|
||||||
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
|
||||||
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
|
||||||
|
|
||||||
redirect(url, RedirectType.push)
|
|
||||||
}, [ctx, nextUrl])
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
repeat: ''
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onChange: z.object({
|
|
||||||
email: z
|
|
||||||
.string()
|
|
||||||
.email()
|
|
||||||
.refine((email) => isValidEmail(email)),
|
|
||||||
username: z.string().refine((username) => isValidUsername(username)),
|
|
||||||
password: z.string().refine((password) => isValidPassword(password)),
|
|
||||||
repeat: z.string().refine((password) => isValidPassword(password))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
try {
|
|
||||||
if (value.password !== value.repeat) {
|
|
||||||
void toastError('Passwords do not match', { label: 'signup error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await ctx!.api.signUpWithPassword({
|
|
||||||
email: value.email,
|
|
||||||
username: value.username,
|
|
||||||
password: value.password
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('signup success', res)
|
|
||||||
} catch (err: any) {
|
|
||||||
void toastError(err, { label: 'signup error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(nextUrl || '/app', RedirectType.push)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense>
|
||||||
<section>
|
<SignupForm />
|
||||||
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
</Suspense>
|
||||||
<form
|
|
||||||
className={cn('flex flex-col gap-6 w-full')}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
void form.handleSubmit()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex flex-col items-center gap-2 text-center'>
|
|
||||||
<h1 className='text-2xl font-bold'>Create an account</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='grid gap-6'>
|
|
||||||
<form.Field
|
|
||||||
name='email'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<Label htmlFor={field.name}>Email</Label>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='email'
|
|
||||||
required
|
|
||||||
placeholder='Email'
|
|
||||||
autoComplete='email'
|
|
||||||
autoFocus={true}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Field
|
|
||||||
name='username'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<Label htmlFor={field.name}>Username</Label>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='text'
|
|
||||||
required
|
|
||||||
placeholder='Username'
|
|
||||||
autoComplete='username'
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e: any) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Field
|
|
||||||
name='password'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<Label htmlFor={field.name}>Password</Label>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='password'
|
|
||||||
required
|
|
||||||
placeholder='Password'
|
|
||||||
// autoFocus={error?.type === 'invalid_password'}
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Field
|
|
||||||
name='repeat'
|
|
||||||
children={(field) => (
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<Label htmlFor={field.name}>Repeat password</Label>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type='password'
|
|
||||||
required
|
|
||||||
placeholder='Password'
|
|
||||||
autoComplete='new-password'
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => [
|
|
||||||
state.canSubmit,
|
|
||||||
state.isSubmitting,
|
|
||||||
state.isTouched
|
|
||||||
]}
|
|
||||||
children={([canSubmit, isSubmitting, isTouched]) => (
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
disabled={!(isTouched && canSubmit && ctx)}
|
|
||||||
className='w-full'
|
|
||||||
>
|
|
||||||
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
|
||||||
<span>Sign up</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
|
||||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
|
||||||
Or continue with
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
className='w-full'
|
|
||||||
onClick={onAuthWithGitHub}
|
|
||||||
>
|
|
||||||
<GitHubIcon />
|
|
||||||
|
|
||||||
<span>Sign up with GitHub</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='text-center text-xs'>
|
|
||||||
Already have an account?{' '}
|
|
||||||
<a
|
|
||||||
href={`/login?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
|
||||||
className='underline underline-offset-4'
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { sanitizeSearchParams } from '@agentic/platform-core'
|
||||||
|
import {
|
||||||
|
isValidEmail,
|
||||||
|
isValidPassword,
|
||||||
|
isValidUsername
|
||||||
|
} from '@agentic/platform-validators'
|
||||||
|
import { useForm } from '@tanstack/react-form'
|
||||||
|
import { Loader2Icon } from 'lucide-react'
|
||||||
|
import { redirect, RedirectType } from 'next/navigation'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useNextUrl,
|
||||||
|
useUnauthenticatedAgentic
|
||||||
|
} from '@/components/agentic-provider'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { GitHubIcon } from '@/icons/github'
|
||||||
|
import { toastError } from '@/lib/notifications'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export function SignupForm() {
|
||||||
|
const ctx = useUnauthenticatedAgentic()
|
||||||
|
const nextUrl = useNextUrl()
|
||||||
|
|
||||||
|
const onAuthWithGitHub = useCallback(async () => {
|
||||||
|
const redirectUri = `${globalThis.location.origin}/auth/github/success?${sanitizeSearchParams({ next: nextUrl }).toString()}`
|
||||||
|
const url = await ctx!.api.initAuthFlowWithGitHub({ redirectUri })
|
||||||
|
|
||||||
|
redirect(url, RedirectType.push)
|
||||||
|
}, [ctx, nextUrl])
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
repeat: ''
|
||||||
|
},
|
||||||
|
validators: {
|
||||||
|
onChange: z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.refine((email) => isValidEmail(email)),
|
||||||
|
username: z.string().refine((username) => isValidUsername(username)),
|
||||||
|
password: z.string().refine((password) => isValidPassword(password)),
|
||||||
|
repeat: z.string().refine((password) => isValidPassword(password))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
if (value.password !== value.repeat) {
|
||||||
|
void toastError('Passwords do not match', { label: 'signup error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ctx!.api.signUpWithPassword({
|
||||||
|
email: value.email,
|
||||||
|
username: value.username,
|
||||||
|
password: value.password
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('signup success', res)
|
||||||
|
} catch (err: any) {
|
||||||
|
void toastError(err, { label: 'signup error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(nextUrl || '/app', RedirectType.push)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className='flex-col flex-1 items-center justify-center w-full max-w-xs'>
|
||||||
|
<form
|
||||||
|
className={cn('flex flex-col gap-6 w-full')}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col items-center gap-2 text-center'>
|
||||||
|
<h1 className='text-2xl font-bold'>Create an account</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-6'>
|
||||||
|
<form.Field
|
||||||
|
name='email'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor={field.name}>Email</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='email'
|
||||||
|
required
|
||||||
|
placeholder='Email'
|
||||||
|
autoComplete='email'
|
||||||
|
autoFocus={true}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name='username'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor={field.name}>Username</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='text'
|
||||||
|
required
|
||||||
|
placeholder='Username'
|
||||||
|
autoComplete='username'
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e: any) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name='password'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor={field.name}>Password</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='password'
|
||||||
|
required
|
||||||
|
placeholder='Password'
|
||||||
|
// autoFocus={error?.type === 'invalid_password'}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Field
|
||||||
|
name='repeat'
|
||||||
|
children={(field) => (
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<Label htmlFor={field.name}>Repeat password</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type='password'
|
||||||
|
required
|
||||||
|
placeholder='Password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => [
|
||||||
|
state.canSubmit,
|
||||||
|
state.isSubmitting,
|
||||||
|
state.isTouched
|
||||||
|
]}
|
||||||
|
children={([canSubmit, isSubmitting, isTouched]) => (
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
disabled={!(isTouched && canSubmit && ctx)}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
{isSubmitting && <Loader2Icon className='animate-spin' />}
|
||||||
|
<span>Sign up</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||||
|
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||||
|
Or continue with
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-full'
|
||||||
|
onClick={onAuthWithGitHub}
|
||||||
|
>
|
||||||
|
<GitHubIcon />
|
||||||
|
|
||||||
|
<span>Sign up with GitHub</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-center text-xs'>
|
||||||
|
Already have an account?{' '}
|
||||||
|
<a
|
||||||
|
href={`/login?${sanitizeSearchParams({ next: nextUrl }).toString()}`}
|
||||||
|
className='underline underline-offset-4'
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
Ładowanie…
Reference in New Issue