### What problem does this PR solve? feat: Add next login page with shadcn/ui #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
49
web/src/pages/demo.tsx
Normal file
49
web/src/pages/demo.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<ModeToggle></ModeToggle>
|
||||
</div>
|
||||
<Button>Destructive</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Demo;
|
||||
246
web/src/pages/login-next/form.tsx
Normal file
246
web/src/pages/login-next/form.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from '@/components/hooks/use-toast';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from '@/components/ui/input-otp';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function SignUpForm() {
|
||||
const { t } = useTranslate('login');
|
||||
|
||||
const FormSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: t('emailPlaceholder'),
|
||||
}),
|
||||
nickname: z.string({ required_error: t('nicknamePlaceholder') }),
|
||||
password: z.string({ required_error: t('passwordPlaceholder') }),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
console.log('🚀 ~ onSubmit ~ data:', data);
|
||||
toast({
|
||||
title: 'You submitted the following values:',
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('emailPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('nicknameLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('nicknamePlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
{t('signUp')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function SignInForm() {
|
||||
const { t } = useTranslate('login');
|
||||
|
||||
const FormSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: t('emailPlaceholder'),
|
||||
}),
|
||||
password: z.string({ required_error: t('passwordPlaceholder') }),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
console.log('🚀 ~ onSubmit ~ data:', data);
|
||||
toast({
|
||||
title: 'You submitted the following values:',
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('emailLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('emailPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('passwordLabel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{t('rememberMe')}
|
||||
</label>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
{t('login')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerifyEmailForm() {
|
||||
const FormSchema = z.object({
|
||||
pin: z.string().min(6, {
|
||||
message: 'Your one-time password must be 6 characters.',
|
||||
}),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
pin: '',
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
console.log('🚀 ~ onSubmit ~ data:', data);
|
||||
toast({
|
||||
title: 'You submitted the following values:',
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pin"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>One-Time Password</FormLabel>
|
||||
<FormControl>
|
||||
<InputOTP maxLength={6} {...field}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Verify
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
88
web/src/pages/login-next/index.tsx
Normal file
88
web/src/pages/login-next/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { DiscordLogoIcon, GitHubLogoIcon } from '@radix-ui/react-icons';
|
||||
import { SignInForm, SignUpForm, VerifyEmailForm } from './form';
|
||||
|
||||
function LoginFooter() {
|
||||
return (
|
||||
<section className="pt-[30px]">
|
||||
<Separator />
|
||||
<p className="text-center pt-[20px]">or continue with</p>
|
||||
<div className="flex gap-4 justify-center pt-[20px]">
|
||||
<GitHubLogoIcon className="w-8 h-8"></GitHubLogoIcon>
|
||||
<DiscordLogoIcon className="w-8 h-8"></DiscordLogoIcon>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SignUpCard() {
|
||||
const { t } = useTranslate('login');
|
||||
|
||||
return (
|
||||
<Card className="w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('signUp')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SignUpForm></SignUpForm>
|
||||
<LoginFooter></LoginFooter>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function SignInCard() {
|
||||
const { t } = useTranslate('login');
|
||||
|
||||
return (
|
||||
<Card className="w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('login')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SignInForm></SignInForm>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function VerifyEmailCard() {
|
||||
// const { t } = useTranslate('login');
|
||||
|
||||
return (
|
||||
<Card className="w-[400px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Verify email</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section className="flex gap-y-6 flex-col">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
We’ve sent a 6-digit code to
|
||||
</p>
|
||||
<p className="text-sm text-blue-500">yifanwu92@gmail.com.</p>
|
||||
</div>
|
||||
<Button>Resend</Button>
|
||||
</div>
|
||||
<VerifyEmailForm></VerifyEmailForm>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
<>
|
||||
<SignUpCard></SignUpCard>
|
||||
<SignInCard></SignInCard>
|
||||
<VerifyEmailCard></VerifyEmailCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
Reference in New Issue
Block a user