added mail service
This commit is contained in:
65
components/auth/.PasswordInput.tsx.todo
Normal file
65
components/auth/.PasswordInput.tsx.todo
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
'use client'
|
||||
//https://gist.github.com/mjbalcueva/b21f39a8787e558d4c536bf68e267398
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { EyeIcon, EyeOffIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input, InputProps } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { FormControl } from '@/components/ui/form'
|
||||
|
||||
const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const disabled = props.value === '' || props.value === undefined ||
|
||||
props.disabled
|
||||
|
||||
return (<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
className={cn('hide-password-toggle pr-10', className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{showPassword && !disabled ? (
|
||||
<EyeIcon
|
||||
className="h-4 w-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<EyeOffIcon
|
||||
className="h-4 w-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{showPassword ? 'Hide password' : 'Show password'}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* hides browsers password toggles */}
|
||||
<style>{`
|
||||
.hide-password-toggle::-ms-reveal,
|
||||
.hide-password-toggle::-ms-clear {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
PasswordInput.displayName = 'PasswordInput'
|
||||
|
||||
export { PasswordInput }
|
||||
|
||||
18
components/auth/BackButton.tsx
Normal file
18
components/auth/BackButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Link from 'next/link'
|
||||
|
||||
type Props = {
|
||||
href: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const BackButton = ({ href, label }: Props) => {
|
||||
return (
|
||||
<Button variant="link" size="sm"
|
||||
className="font-normal w-full" asChild>
|
||||
<Link href={href}>{label}</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
55
components/auth/CardWrapper.tsx
Normal file
55
components/auth/CardWrapper.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||
import { Header } from '@/components/auth/Header'
|
||||
import { Social } from '@/components/auth/Social'
|
||||
import { BackButton } from '@/components/auth/BackButton'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
headerLabel: string
|
||||
headerTitle: string
|
||||
backButtonLabel: string
|
||||
backButtonHref: string
|
||||
showSocial?: boolean
|
||||
continueWithLabel?: string
|
||||
}
|
||||
|
||||
export const CardWrapper = ({
|
||||
children,
|
||||
headerLabel,
|
||||
headerTitle,
|
||||
backButtonLabel,
|
||||
backButtonHref,
|
||||
showSocial,
|
||||
continueWithLabel,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Card
|
||||
className="max-w-[414px] w-[100%] shadow-md md:min-w-[414px] sm:w-full">
|
||||
<CardHeader>
|
||||
<Header label={headerLabel} title={headerTitle}/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{children}
|
||||
</CardContent>
|
||||
{showSocial && <CardFooter className="flex-wrap">
|
||||
<div className="relative flex-none w-[100%] mb-4"
|
||||
style={{ display: 'block' }}>
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t"></span>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span
|
||||
className="bg-background px-2 text-muted-foreground">{continueWithLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*<Separator className="my-4"/>*/}
|
||||
<Social/>
|
||||
</CardFooter>}
|
||||
<CardFooter>
|
||||
<BackButton label={backButtonLabel} href={backButtonHref}/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
25
components/auth/ErrorCard.tsx
Normal file
25
components/auth/ErrorCard.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { CardWrapper } from '@/components/auth/CardWrapper'
|
||||
import { AUTH_LOGIN_URL } from '@/config/routes'
|
||||
import { useI18n } from '@/locales/client'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
|
||||
const ErrorCard = () => {
|
||||
const t = useI18n()
|
||||
return (
|
||||
<CardWrapper
|
||||
headerLabel={t('auth.form.error.header_label')}
|
||||
headerTitle={t('auth.title')}
|
||||
backButtonLabel={t('auth.form.error.back_button_label')}
|
||||
backButtonHref={AUTH_LOGIN_URL}
|
||||
>
|
||||
<div className="w-full flex items-center justify-center">
|
||||
<TriangleAlert className="w-4 h-4 text-destructive"/>
|
||||
<p>ssss</p>
|
||||
</div>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorCard
|
||||
20
components/auth/Header.tsx
Normal file
20
components/auth/Header.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Poppins } from 'next/font/google'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const font = Poppins({
|
||||
subsets: ['latin'], weight: ['600'],
|
||||
})
|
||||
|
||||
type Props = {
|
||||
label: string, title: string
|
||||
}
|
||||
|
||||
export const Header = ({ label, title }: Props) => {
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-y-4 items-center justify-center">
|
||||
<h1 className={cn('text-3xl font-semibold', font.className)}>
|
||||
🔐 {title || 'Auth'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">{label}</p>
|
||||
</div>)
|
||||
}
|
||||
25
components/auth/LoginButton.tsx
Normal file
25
components/auth/LoginButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { AUTH_LOGIN_URL } from '@/config/routes'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
mode?: 'modal' | 'redirect'
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const LoginButton = ({
|
||||
children, mode = 'redirect', asChild,
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const onClick = () => router.push(AUTH_LOGIN_URL)
|
||||
|
||||
if (mode === 'modal') {
|
||||
return <span>TODO: Implement modal</span>
|
||||
}
|
||||
|
||||
return <span onClick={onClick} className="cursor-pointer">{children}</span>
|
||||
}
|
||||
|
||||
export default LoginButton
|
||||
112
components/auth/LoginForm.tsx
Normal file
112
components/auth/LoginForm.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
|
||||
import { infer as zInfer } from 'zod'
|
||||
import { useState, useTransition } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CardWrapper } from '@/components/auth/CardWrapper'
|
||||
import { useI18n } from '@/locales/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import FormError from '@/components/FormError'
|
||||
import FormSuccess from '@/components/FormSuccess'
|
||||
import { login } from '@/actions/login'
|
||||
import { LoginSchema } from '@/schemas'
|
||||
import { AUTH_REGISTER_URL } from '@/config/routes'
|
||||
|
||||
export const LoginForm = () => {
|
||||
const t = useI18n()
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const urlError = searchParams.get('error') === 'OAuthAccountNotLinked'
|
||||
? t('auth.form.error.email_in_use')
|
||||
: ''
|
||||
|
||||
const [error, setError] = useState<string | undefined>('')
|
||||
const [success, setSuccess] = useState<string | undefined>('')
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const form = useForm<zInfer<typeof LoginSchema>>({
|
||||
resolver: zodResolver(LoginSchema), defaultValues: {
|
||||
email: '', password: '',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: zInfer<typeof LoginSchema>) => {
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
startTransition(() => {
|
||||
login(values).then((data) => {
|
||||
// @ts-ignore
|
||||
setError(t(data?.error))
|
||||
// @ts-ignore
|
||||
setSuccess(t(data?.success))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (<CardWrapper
|
||||
headerLabel={t('auth.form.login.header_label')}
|
||||
headerTitle={t('auth.title')}
|
||||
backButtonLabel={t('auth.form.login.back_button_label')}
|
||||
backButtonHref={AUTH_REGISTER_URL}
|
||||
showSocial
|
||||
continueWithLabel={t('form.label.continue_with')}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField control={form.control} name="email"
|
||||
render={({ field }) => (<FormItem>
|
||||
<FormLabel>{t('form.label.email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder={t('form.placeholder.email')}
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs"/>
|
||||
</FormItem>)}/>
|
||||
{/*Password*/}
|
||||
<FormField control={form.control} name="password"
|
||||
render={({ field }) => (<FormItem>
|
||||
<FormLabel>{t('form.label.password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder="******"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs"/>
|
||||
</FormItem>)}/>
|
||||
</div>
|
||||
<FormSuccess message={success}/>
|
||||
<FormError message={error || urlError}/>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{t('form.label.login')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardWrapper>)
|
||||
}
|
||||
|
||||
//1:30:00
|
||||
14
components/auth/Navbar.tsx
Normal file
14
components/auth/Navbar.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client'
|
||||
//import { useScopedI18n } from '@/locales/client'
|
||||
import LocaleSwitcher from '@/components/LocaleSwitcher'
|
||||
|
||||
export default function Navbar () {
|
||||
//const t = useScopedI18n('navbar')
|
||||
|
||||
return (
|
||||
<nav className="flex justify-between top-0 absolute w-full px-3.5 py-1.5">
|
||||
<div>Logo</div>
|
||||
<LocaleSwitcher/>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
123
components/auth/RegisterForm.tsx
Normal file
123
components/auth/RegisterForm.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import { infer as zInfer } from 'zod'
|
||||
import { useState, useTransition } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CardWrapper } from '@/components/auth/CardWrapper'
|
||||
import { useI18n } from '@/locales/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import FormError from '@/components/FormError'
|
||||
import FormSuccess from '@/components/FormSuccess'
|
||||
|
||||
import { register } from '@/actions/register'
|
||||
import { RegisterSchema } from '@/schemas'
|
||||
import { AUTH_LOGIN_URL } from '@/config/routes'
|
||||
|
||||
export const RegisterForm = () => {
|
||||
// const [currentPassword, setCurrentPassword] = useState('')
|
||||
// const [password, setPassword] = useState('')
|
||||
// const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
||||
const [error, setError] = useState<string | undefined>('')
|
||||
const [success, setSuccess] = useState<string | undefined>('')
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const form = useForm<zInfer<typeof RegisterSchema>>({
|
||||
resolver: zodResolver(RegisterSchema), defaultValues: {
|
||||
email: '', password: '', name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: zInfer<typeof RegisterSchema>) => {
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
startTransition(() => {
|
||||
register(values).then((data) => {
|
||||
// @ts-ignore
|
||||
setError(t(data?.error))
|
||||
// @ts-ignore
|
||||
setSuccess(t(data?.success))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (<CardWrapper
|
||||
headerLabel={t('auth.form.register.header_label')}
|
||||
headerTitle={t('auth.title')}
|
||||
backButtonLabel={t('auth.form.register.back_button_label')}
|
||||
backButtonHref={AUTH_LOGIN_URL}
|
||||
showSocial
|
||||
continueWithLabel={t('form.label.continue_with')}
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/*Name*/}
|
||||
<FormField control={form.control} name="name"
|
||||
render={({ field }) => (<FormItem>
|
||||
<FormLabel>{t('form.label.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder={t('form.placeholder.name')}
|
||||
type="text"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs"/>
|
||||
</FormItem>)}/>
|
||||
{/*Email*/}
|
||||
<FormField control={form.control} name="email"
|
||||
render={({ field }) => (<FormItem>
|
||||
<FormLabel>{t('form.label.email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
placeholder={t('form.placeholder.email')}
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs"/>
|
||||
</FormItem>)}/>
|
||||
{/*Password*/}
|
||||
<FormField control={form.control} name="password"
|
||||
render={({ field }) => (<FormItem className="zhopa">
|
||||
<FormLabel>{t('form.label.password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={isPending}
|
||||
type="password"
|
||||
placeholder="******"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs"/>
|
||||
</FormItem>)}/>
|
||||
</div>
|
||||
<FormSuccess message={success}/>
|
||||
<FormError message={error}/>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{t('form.label.register')}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardWrapper>)
|
||||
}
|
||||
31
components/auth/Social.tsx
Normal file
31
components/auth/Social.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { FcGoogle } from 'react-icons/fc'
|
||||
import { FaFacebook, FaGithub } from 'react-icons/fa'
|
||||
//import { RiTwitterXLine } from 'react-icons/ri'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SignInProvider } from '@/actions/login'
|
||||
|
||||
export const Social = () => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full gap-x-2">
|
||||
<Button size="lg" className="w-full" variant="outline"
|
||||
onClick={() => SignInProvider('google')}>
|
||||
<FcGoogle className="w-5 h-5"/>
|
||||
</Button>
|
||||
<Button size="lg" className="w-full" variant="outline"
|
||||
onClick={() => SignInProvider('github')}>
|
||||
<FaGithub className="w-5 h-5"/>
|
||||
</Button>
|
||||
{/*<Button size="lg" className="w-full" variant="outline" onClick={() => {}}>
|
||||
<RiTwitterXLine className="w-5 h-5"/>
|
||||
</Button>*/}
|
||||
{/*<Button size="lg" className="w-full" variant="outline"
|
||||
onClick={() => SignInProvider('facebook')}>
|
||||
<FaFacebook className="w-5 h-5" style={{ color: '#1877F2' }}/>
|
||||
</Button>*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user