added mail service

This commit is contained in:
2024-04-10 21:24:25 +03:00
parent c76d4b9717
commit 78107d4ec7
80 changed files with 3478 additions and 329 deletions

View 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 }

View 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>
)
}

View 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>
)
}

View 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

View 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>)
}

View 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

View 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

View 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>
)
}

View 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>)
}

View 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>
)
}