diff --git a/002_source/cms/public/assets/logo-github.svg b/002_source/cms/public/assets/logo-github.svg new file mode 100644 index 0000000..c3c6961 --- /dev/null +++ b/002_source/cms/public/assets/logo-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/002_source/cms/public/locales/dev/sign_in.json b/002_source/cms/public/locales/dev/sign_in.json new file mode 100644 index 0000000..158b40a --- /dev/null +++ b/002_source/cms/public/locales/dev/sign_in.json @@ -0,0 +1,31 @@ +{ + "sign-in": "Sign in / 登入", + "dont-have-an-account": "Don't have an account ?", + "continue-with_fh": "以", + "continue-with_sh": "繼續", + "forgot-password": "Forgot password", + "user": "用戶名稱", + "password": "密碼", + "email-address": "用戶電郵", + "first-name-is-required": "名字是必填项", + "last-name-is-required": "姓氏不能为空", + "email-is-required": "邮箱不能为空", + "password-should-be-at-least-6-characters": "密码至少需要6个字符", + "you-must-accept-the-terms-and-conditions": "您必须接受条款和条件", + "sign-up": "注册", + "already-have-an-account": "已有账号?", + "or": "或", + "create-account": "创建账号", + "created-users-are-not-persisted": "已创建的用户不会被保存", + "i-have-read-the": "我已阅读", + "terms-and-conditions": "用户协议和隐私政策", + "e.g.": "例如:", + "first-name": "名", + "last-name": "姓", + "hello": "world", + "welcome-title": "Welcome to devias kit pro", + "welcome-notes": "A professional template that comes with ready-to-use MUI components developed with one common goal in mind, help you build faster & beautiful applications.", + "reset-password": "重置密码", + "back-to-login": "返回登录", + "send-recovery-link": "发送恢复链接" +} diff --git a/002_source/cms/src/components/auth/custom/OAuthProvider.tsx b/002_source/cms/src/components/auth/custom/OAuthProvider.tsx new file mode 100644 index 0000000..b9a2902 --- /dev/null +++ b/002_source/cms/src/components/auth/custom/OAuthProvider.tsx @@ -0,0 +1,5 @@ +export interface OAuthProvider { + id: 'google' | 'discord' | 'github'; + name: string; + logo: string; +} diff --git a/002_source/cms/src/components/auth/custom/oAuthProviders.tsx b/002_source/cms/src/components/auth/custom/oAuthProviders.tsx new file mode 100644 index 0000000..0b18910 --- /dev/null +++ b/002_source/cms/src/components/auth/custom/oAuthProviders.tsx @@ -0,0 +1,14 @@ +'use client'; + +import type { OAuthProvider } from './OAuthProvider'; + +// export const oAuthProviders = [ +// { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, +// { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +// ] satisfies OAuthProvider[]; + +export const oAuthProviders = [ + { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, + { id: 'github', name: 'Github', logo: '/assets/logo-github.svg' }, + // { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +] satisfies OAuthProvider[]; diff --git a/002_source/cms/src/components/auth/custom/reset-password-form.tsx b/002_source/cms/src/components/auth/custom/reset-password-form.tsx deleted file mode 100644 index f2f2a0f..0000000 --- a/002_source/cms/src/components/auth/custom/reset-password-form.tsx +++ /dev/null @@ -1,86 +0,0 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import FormHelperText from '@mui/material/FormHelperText'; -import InputLabel from '@mui/material/InputLabel'; -import OutlinedInput from '@mui/material/OutlinedInput'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { Controller, useForm } from 'react-hook-form'; -import { z as zod } from 'zod'; - -import { paths } from '@/paths'; -import { authClient } from '@/lib/auth/custom/client'; -import { DynamicLogo } from '@/components/core/logo'; - -const schema = zod.object({ email: zod.string().min(1, { message: 'Email is required' }).email() }); - -type Values = zod.infer; - -const defaultValues = { email: '' } satisfies Values; - -export function ResetPasswordForm(): React.JSX.Element { - const [isPending, setIsPending] = React.useState(false); - - const { - control, - handleSubmit, - setError, - formState: { errors }, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onSubmit = React.useCallback( - async (values: Values): Promise => { - setIsPending(true); - - const { error } = await authClient.resetPassword(values); - - if (error) { - setError('root', { type: 'server', message: error }); - setIsPending(false); - return; - } - - setIsPending(false); - - // Redirect to confirm password reset - }, - [setError] - ); - - return ( - - - - - - - Reset password - - - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - {errors.root ? {errors.root.message} : null} - - Send recovery link - - - - - ); -} diff --git a/002_source/cms/src/components/auth/custom/sign-in-form.tsx b/002_source/cms/src/components/auth/custom/sign-in-form/index.tsx similarity index 78% rename from 002_source/cms/src/components/auth/custom/sign-in-form.tsx rename to 002_source/cms/src/components/auth/custom/sign-in-form/index.tsx index 5ab6b8b..39df856 100644 --- a/002_source/cms/src/components/auth/custom/sign-in-form.tsx +++ b/002_source/cms/src/components/auth/custom/sign-in-form/index.tsx @@ -1,11 +1,13 @@ 'use client'; + // RULES: // refer to ticket REQ0016 for login flow - +// import * as React from 'react'; import RouterLink from 'next/link'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; +import { LoadingButton } from '@mui/lab'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -20,6 +22,7 @@ import Typography from '@mui/material/Typography'; import { Eye as EyeIcon } from '@phosphor-icons/react/dist/ssr/Eye'; import { EyeSlash as EyeSlashIcon } from '@phosphor-icons/react/dist/ssr/EyeSlash'; import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import { z as zod } from 'zod'; import { paths } from '@/paths'; @@ -27,30 +30,25 @@ import { authClient } from '@/lib/auth/custom/client'; import { useUser } from '@/hooks/use-user'; import { DynamicLogo } from '@/components/core/logo'; import { toast } from '@/components/core/toaster'; -import { pb } from '@/lib/pb'; -interface OAuthProvider { - id: 'google' | 'discord'; - name: string; - logo: string; -} +import { OAuthProvider } from '../OAuthProvider'; +import { oAuthProviders } from '../oAuthProviders'; -const oAuthProviders = [ - { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, - { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, -] satisfies OAuthProvider[]; +// interface OAuthProvider { +// id: 'google' | 'discord' | 'github'; +// name: string; +// logo: string; +// } -const schema = zod.object({ - email: zod.string().min(1, { message: 'Email is required' }).email(), - password: zod.string().min(1, { message: 'Password is required' }), -}); - -type Values = zod.infer; - -const defaultValues = { email: 'admin@123.com', password: 'admin@123.com' } satisfies Values; +// const oAuthProviders = [ +// { id: 'google', name: 'Google', logo: '/assets/logo-google.svg' }, +// { id: 'github', name: 'Github', logo: '/assets/logo-github.svg' }, +// // { id: 'discord', name: 'Discord', logo: '/assets/logo-discord.svg' }, +// ] satisfies OAuthProvider[]; export function SignInForm(): React.JSX.Element { const router = useRouter(); + const { t } = useTranslation(['sign_in']); const { checkSession } = useUser(); @@ -58,6 +56,14 @@ export function SignInForm(): React.JSX.Element { const [isPending, setIsPending] = React.useState(false); + const schema = zod.object({ + email: zod.string().min(1, { message: 'Email is required' }).email(), + password: zod.string().min(1, { message: 'Password is required' }), + }); + + type Values = zod.infer; + const defaultValues = { email: '', password: '' } satisfies Values; + const { control, handleSubmit, @@ -98,14 +104,16 @@ export function SignInForm(): React.JSX.Element { // UserProvider, for this case, will not refresh the router // After refresh, GuestGuard will handle the redirect - router.refresh(); + + // TODO: resume me + // router.refresh(); }, [checkSession, router, setError] ); return ( - + - + - Sign in + {t('sign-in')} - Don't have an account?{' '} + {t('dont-have-an-account')}{' '} - Sign up + {t('sign-up')} - + {oAuthProviders.map( (provider): React.JSX.Element => ( - Continue with {provider.name} + {t('continue-with_fh')} {provider.name} {t('continue-with_sh')} ) )} - or + {t('or')} @@ -173,10 +184,12 @@ export function SignInForm(): React.JSX.Element { name="email" render={({ field }) => ( - Email address + {t('email-address')} {errors.email ? {errors.email.message} : null} @@ -187,9 +200,10 @@ export function SignInForm(): React.JSX.Element { name="password" render={({ field }) => ( - Password + {t('password')} {errors.password ? {errors.password.message} : null} )} /> {errors.root ? {errors.root.message} : null} - - Sign in - + {t('sign-in')} + @@ -232,7 +249,7 @@ export function SignInForm(): React.JSX.Element { href={paths.auth.custom.resetPassword} variant="subtitle2" > - Forgot password? + {t('forgot-password')} ? @@ -240,7 +257,7 @@ export function SignInForm(): React.JSX.Element { - user:{' '} + {t('user')}:{' '} admin@123.com {' '} - password{' '} + {t('password')}{' '} - user{' '} + {t('user')}{' '} sofia@devias.io {' '} - password{' '} + {t('password')}{' '} value, 'You must accept the terms and conditions'), -}); - -type Values = zod.infer; - -const defaultValues = { firstName: '', lastName: '', email: '', password: '', terms: false } satisfies Values; - -export function SignUpForm(): React.JSX.Element { - const router = useRouter(); - - const { checkSession } = useUser(); - - const [isPending, setIsPending] = React.useState(false); - - const { - control, - handleSubmit, - setError, - formState: { errors }, - } = useForm({ defaultValues, resolver: zodResolver(schema) }); - - const onAuth = React.useCallback(async (providerId: OAuthProvider['id']): Promise => { - setIsPending(true); - - const { error } = await authClient.signInWithOAuth({ provider: providerId }); - - if (error) { - setIsPending(false); - toast.error(error); - return; - } - - setIsPending(false); - - // Redirect to OAuth provider - }, []); - - const onSubmit = React.useCallback( - async (values: Values): Promise => { - setIsPending(true); - - const { error } = await authClient.signUp(values); - - if (error) { - setError('root', { type: 'server', message: error }); - setIsPending(false); - return; - } - - // Refresh the auth state - await checkSession?.(); - - // UserProvider, for this case, will not refresh the router - // After refresh, GuestGuard will handle the redirect - router.refresh(); - }, - [checkSession, router, setError] - ); - - return ( - - - - - - - - Sign up - - Already have an account?{' '} - - Sign in - - - - - - {oAuthProviders.map( - (provider): React.JSX.Element => ( - } - key={provider.id} - onClick={(): void => { - onAuth(provider.id).catch(() => { - // noop - }); - }} - variant="outlined" - > - Continue with {provider.name} - - ) - )} - - or - - - ( - - First name - - {errors.firstName ? {errors.firstName.message} : null} - - )} - /> - ( - - Last name - - {errors.lastName ? {errors.lastName.message} : null} - - )} - /> - ( - - Email address - - {errors.email ? {errors.email.message} : null} - - )} - /> - ( - - Password - - {errors.password ? {errors.password.message} : null} - - )} - /> - ( - - } - label={ - - I have read the terms and conditions - - } - /> - {errors.terms ? {errors.terms.message} : null} - - )} - /> - {errors.root ? {errors.root.message} : null} - - Create account - - - - - Created users are not persisted - - ); -} diff --git a/002_source/cms/src/components/auth/split-layout.tsx b/002_source/cms/src/components/auth/split-layout.tsx deleted file mode 100644 index 43bdf92..0000000 --- a/002_source/cms/src/components/auth/split-layout.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; - -export interface SplitLayoutProps { - children: React.ReactNode; -} - -export function SplitLayout({ children }: SplitLayoutProps): React.JSX.Element { - return ( - - - - - Welcome to Devias Kit PRO - - A professional template that comes with ready-to-use MUI components developed with one common goal in - mind, help you build faster & beautiful applications. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {children} - - - - ); -} diff --git a/002_source/cms/src/components/auth/split-layout/index.tsx b/002_source/cms/src/components/auth/split-layout/index.tsx new file mode 100644 index 0000000..5e5c7c2 --- /dev/null +++ b/002_source/cms/src/components/auth/split-layout/index.tsx @@ -0,0 +1,207 @@ +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +import LoginAvif from './login_bg.avif'; + +export interface SplitLayoutProps { + children: React.ReactNode; +} + +export function SplitLayout({ children }: SplitLayoutProps): React.JSX.Element { + const { t } = useTranslation(['sign_in']); + + return ( + + + + + + + {t('welcome-title')} + {t('welcome-notes')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + ); +} diff --git a/002_source/cms/src/components/auth/split-layout/login_bg.avif b/002_source/cms/src/components/auth/split-layout/login_bg.avif new file mode 100644 index 0000000..66024d4 Binary files /dev/null and b/002_source/cms/src/components/auth/split-layout/login_bg.avif differ diff --git a/002_source/cms/src/lib/auth/custom/client.ts b/002_source/cms/src/lib/auth/custom/client.ts index 6a61234..6f22116 100644 --- a/002_source/cms/src/lib/auth/custom/client.ts +++ b/002_source/cms/src/lib/auth/custom/client.ts @@ -22,7 +22,7 @@ export interface SignUpParams { } export interface SignInWithOAuthParams { - provider: 'google' | 'discord'; + provider: 'google' | 'discord' | 'github'; } export interface SignInWithPasswordParams {