added mail service
This commit is contained in:
19
components/FormError.tsx
Normal file
19
components/FormError.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
|
||||
type Props = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const FormError = ({ message }: Props) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
|
||||
<TriangleAlert className="w-4 h-4"/>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormError
|
||||
19
components/FormSuccess.tsx
Normal file
19
components/FormSuccess.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CircleCheck } from 'lucide-react'
|
||||
|
||||
type Props = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const FormSuccess = ({ message }: Props) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-lime-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-lime-800">
|
||||
<CircleCheck className="w-4 h-4"/>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormSuccess
|
||||
24
components/LocaleSwitcher.tsx
Normal file
24
components/LocaleSwitcher.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import { useChangeLocale, useCurrentLocale } from '@/locales/client'
|
||||
import { LC, type loc } from '@/config/locales'
|
||||
import { ChangeEvent } from 'react'
|
||||
import styles from '@/styles/LocaleSwitcher.module.scss'
|
||||
|
||||
export default function LocaleSwitcher () {
|
||||
const changeLocale = useChangeLocale()
|
||||
const locale = useCurrentLocale()
|
||||
const selectHandler = (e: ChangeEvent<HTMLSelectElement>) => changeLocale(
|
||||
e.target.value as loc)
|
||||
|
||||
return (
|
||||
//@ts-ignore
|
||||
<select onChange={selectHandler} defaultValue={locale}
|
||||
className={styles['yo-locale-switcher']}>
|
||||
{LC.map(item => (
|
||||
<option key={item.iso} value={item.code}>
|
||||
{item.iso.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
26
components/TranslateClientFragment.tsx
Normal file
26
components/TranslateClientFragment.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useI18n } from '@/locales/client'
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
}
|
||||
|
||||
const _ = (message: string): string => {
|
||||
const t = useI18n()
|
||||
if (message.startsWith('["')) {
|
||||
const data = JSON.parse(message)
|
||||
if (data.length > 1) {
|
||||
message = data.shift()
|
||||
// @ts-ignore
|
||||
return t(message, ...data)
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return t(message)
|
||||
}
|
||||
|
||||
const TranslateClientFragment = ({ message }: Props) => {
|
||||
return <>{_(message)}</>
|
||||
}
|
||||
|
||||
export default TranslateClientFragment
|
||||
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>
|
||||
)
|
||||
}
|
||||
56
components/ui/button.tsx
Normal file
56
components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
components/ui/card.tsx
Normal file
79
components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
152
components/ui/form.tsx
Normal file
152
components/ui/form.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import TranslateClientFragment from '@/components/TranslateClientFragment'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue)
|
||||
|
||||
const FormField = <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> ({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`, ...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue)
|
||||
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
const FormLabel = React.forwardRef<React.ElementRef<typeof LabelPrimitive.Root>, React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
|
||||
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
|
||||
({ ...props }, ref) => {
|
||||
const {
|
||||
error,
|
||||
formItemId,
|
||||
formDescriptionId,
|
||||
formMessageId,
|
||||
} = useFormField()
|
||||
|
||||
return (<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
let body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-sm font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{!process.env.IS_SERVER_FLAG && typeof body === 'string' &&
|
||||
body.includes('schema.message')
|
||||
? <TranslateClientFragment message={body}/>
|
||||
: body}
|
||||
</p>)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
25
components/ui/input.tsx
Normal file
25
components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
components/ui/label.tsx
Normal file
26
components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
31
components/ui/separator.tsx
Normal file
31
components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
Reference in New Issue
Block a user