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

19
components/FormError.tsx Normal file
View 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

View 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

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

View 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

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

56
components/ui/button.tsx Normal file
View 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
View 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
View 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
View 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
View 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 }

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