diff options
author | polwex <polwex@sortug.com> | 2025-05-15 10:13:00 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-15 10:13:00 +0700 |
commit | d56594d3289002566f4653d607f0837befd65109 (patch) | |
tree | f69685b458419566a78727ce6a8cecd0cdc269a5 /src/components | |
parent | 04509d9207603d9055cf022051763ec05c9214d6 (diff) |
wtf man
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/Login.tsx | 305 | ||||
-rw-r--r-- | src/components/Login2.tsx | 148 | ||||
-rw-r--r-- | src/components/actiontest.tsx | 27 | ||||
-rw-r--r-- | src/components/counter.tsx | 12 | ||||
-rw-r--r-- | src/components/ui/button.tsx | 58 | ||||
-rw-r--r-- | src/components/ui/card.tsx | 68 | ||||
-rw-r--r-- | src/components/ui/form.tsx | 165 | ||||
-rw-r--r-- | src/components/ui/input.tsx | 19 | ||||
-rw-r--r-- | src/components/ui/label.tsx | 24 | ||||
-rw-r--r-- | src/components/ui/select.tsx | 179 | ||||
-rw-r--r-- | src/components/ui/sonner.tsx | 23 | ||||
-rw-r--r-- | src/components/ui/spinner.tsx | 8 |
12 files changed, 1033 insertions, 3 deletions
diff --git a/src/components/Login.tsx b/src/components/Login.tsx new file mode 100644 index 0000000..5747d06 --- /dev/null +++ b/src/components/Login.tsx @@ -0,0 +1,305 @@ +"use client"; + +import { postLogin, postRegister } from "@/actions/login"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardHeader, + CardDescription, + CardContent, + CardFooter, + CardTitle, +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { useActionState } from "react"; + +const FormSchema = z.object({ + username: z.string().min(2, { + message: "Username must be at least 2 characters.", + }), + password: z.string().min(2, { + message: "Password must be at least 2 characters.", + }), +}); +export default function AuthScreen() { + return <OOldform />; +} + +function OOldform() { + const [state, formAction, isPending] = useActionState< + { msg: string }, + FormData + >(postLogin, { msg: "init" }); + return ( + <form action={formAction}> + {state.msg} + <label> + Username + <input type="text" placeholder="shadcn" name="username" /> + </label> + <label className="flex justify-between"> + <span>Password</span> + <a + href="#" + className="ml-auto text-sm underline-offset-4 hover:underline" + > + Forgot your password? + </a> + <input type="password" placeholder="..." name="password" /> + </label> + <button type="submit" className="w-full"> + Login + </button> + <div className="text-center text-sm"> + Don't have an account?{" "} + <a href="#" className="underline underline-offset-4"> + Sign up + </a> + </div> + </form> + ); +} +function Oldform() { + const [state, formAction, isPending] = useActionState< + { msg: string }, + FormData + >(postLogin, { msg: "init" }); + return ( + <form action={formAction}> + {state.msg} + <div className="flex flex-col gap-6"> + <Card> + <CardHeader className="text-center"> + <CardTitle className="text-xl">Welcome back</CardTitle> + <CardDescription>Login to Sorlang</CardDescription> + <p>{state.msg}</p> + </CardHeader> + <CardContent> + <div className="grid gap-6"> + <div className="grid gap-6"> + <Label> + Username + <Input placeholder="shadcn" name="username" /> + </Label> + <Label className="flex justify-between"> + <span>Password</span> + <a + href="#" + className="ml-auto text-sm underline-offset-4 hover:underline" + > + Forgot your password? + </a> + <Input type="password" placeholder="..." name="password" /> + </Label> + </div> + <Button type="submit" className="w-full"> + Login + </Button> + <div className="text-center text-sm"> + Don't have an account?{" "} + <a href="#" className="underline underline-offset-4"> + Sign up + </a> + </div> + </div> + </CardContent> + </Card> + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary "> + By clicking continue, you agree to our{" "} + <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. + </div> + </div> + </form> + ); +} + +function Register({ setRegister }: { setRegister: (b: boolean) => void }) { + const form = useForm<z.infer<typeof FormSchema>>({ + resolver: zodResolver(FormSchema), + defaultValues: { + username: "", + password: "", + }, + }); + + async function onSubmit(data: z.infer<typeof FormSchema>) { + const body = JSON.stringify({ + name: data.username, + creds: data.password, + }); + const opts = { + method: "POST", + headers: { "Content-type": "application/json" }, + body, + }; + const res = await fetch("/api/db/user/new", opts); + const j = await res.json(); + console.log(j); + if ("error" in j) toast(`Error: ${j.error}`); + else { + toast("Register successful"); + } + } + + return ( + <Card> + <CardTitle>Register</CardTitle> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="w-2/3 space-y-6" + > + <CardContent> + <FormField + control={form.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>Username</FormLabel> + <FormControl> + <Input placeholder="shadcn" {...field} /> + </FormControl> + <FormDescription> + This is your public display name. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel>Password</FormLabel> + <FormControl> + <Input type="password" placeholder="..." {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </CardContent> + <CardFooter> + <Button type="submit">Submit</Button> + </CardFooter> + </form> + </Form> + </Card> + ); +} + +function LoginForm({ + className, + ...props +}: React.ComponentPropsWithoutRef<"div">) { + const form = useForm<z.infer<typeof FormSchema>>({ + resolver: zodResolver(FormSchema), + defaultValues: { + username: "", + password: "", + }, + }); + async function onSubmit(data: z.infer<typeof FormSchema>) { + console.log("oh hai"); + const body = JSON.stringify({ + name: data.username, + creds: data.password, + }); + const opts = { + method: "POST", + headers: { "Content-type": "application/json" }, + body, + }; + const res = await fetch("/api/login", opts); + const j = await res.json(); + console.log({ j }); + if ("error" in j) toast(`Error! ${j.error}`); + else { + toast("Login successful"); + } + } + return ( + <div className={cn("flex flex-col gap-6", className)} {...props}> + <Card> + <CardHeader className="text-center"> + <CardTitle className="text-xl">Welcome back</CardTitle> + <CardDescription>Login to Sorlang</CardDescription> + </CardHeader> + <CardContent> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="grid gap-6"> + <div className="grid gap-6"> + <FormField + control={form.control} + name="username" + render={({ field }) => ( + <FormItem> + <FormLabel>Username</FormLabel> + <FormControl> + <Input placeholder="shadcn" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="password" + render={({ field }) => ( + <FormItem> + <FormLabel className="flex justify-between"> + <span>Password</span> + <a + href="#" + className="ml-auto text-sm underline-offset-4 hover:underline" + > + Forgot your password? + </a> + </FormLabel> + <FormControl> + <Input type="password" placeholder="..." {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + <Button type="submit" className="w-full"> + Login + </Button> + <div className="text-center text-sm"> + Don't have an account?{" "} + <a href="#" className="underline underline-offset-4"> + Sign up + </a> + </div> + </div> + </form> + </Form> + </CardContent> + </Card> + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary "> + By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "} + and <a href="#">Privacy Policy</a>. + </div> + </div> + ); +} diff --git a/src/components/Login2.tsx b/src/components/Login2.tsx new file mode 100644 index 0000000..2164b1a --- /dev/null +++ b/src/components/Login2.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { FormState, postLogin, postRegister } from "@/actions/login"; + +import { useActionState, useEffect, useState } from "react"; +import { + Card, + CardHeader, + CardDescription, + CardContent, + CardFooter, + CardTitle, +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { useRouter } from "waku"; + +export default function AuthScreen() { + const [isReg, setReg] = useState(false); + return <OOldform isReg={isReg} toggle={() => setReg((b) => !b)} />; +} + +function OOldform({ isReg, toggle }: { isReg: boolean; toggle: () => void }) { + const regstrings = { + title: "Welcome to Sorlang", + desc: "Sign up", + button: "Sign up", + toggle: "Have an account?", + toggle2: "Login", + }; + const logstrings = { + title: "Welcome back", + desc: "Login to Sorlang", + button: "Login", + toggle: "Don't have an account?", + toggle2: "Sign up", + }; + const [strings, setStrings] = useState(logstrings); + + useEffect(() => { + if (isReg) setStrings(regstrings); + else setStrings(logstrings); + }, [isReg]); + + const [state, formAction, isPending] = useActionState<FormState, FormData>( + isReg ? postRegister : postLogin, + {}, + "/login", + ); + // const nav = useRouter(); + // useEffect(() => { + // if (state.success) nav.replace("/"); + // }, [state]); + return ( + <form action={formAction}> + <div className="flex flex-col gap-6"> + <Card> + <CardHeader className="text-center"> + <CardTitle className="text-xl">{strings.title}</CardTitle> + <CardDescription>{strings.desc}</CardDescription> + </CardHeader> + <CardContent> + <div className="grid gap-6"> + <div className="grid gap-6"> + <Label> + Username + <Input placeholder="shadcn" name="username" /> + {state.name && <p>{state.name}</p>} + </Label> + <Label> + {isReg ? ( + "Password" + ) : ( + <Label className="flex justify-between"> + <span>Password</span> + <a + href="#" + className="ml-auto text-sm underline-offset-4 hover:underline" + > + Forgot your password? + </a> + </Label> + )} + <Input type="password" placeholder="..." name="password" /> + {state.password && <p>{state.password}</p>} + </Label> + </div> + <Button type="submit" className="w-full"> + {strings.button} + </Button> + <div className="text-center text-sm"> + {strings.toggle} + <a + onClick={toggle} + href="#" + className="underline underline-offset-4" + > + {strings.toggle2} + </a> + </div> + </div> + {state.error && <p className="text-red">{state.error}</p>} + </CardContent> + </Card> + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary "> + By clicking continue, you agree to our{" "} + <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. + </div> + </div> + </form> + // <form action={formAction} aria-disabled={isPending}> + // {state.msg} + // <label> + // Username + // <input type="text" placeholder="shadcn" name="username" /> + // </label> + // <label className="flex justify-between"> + // <span>Password</span> + // <a + // href="#" + // className="ml-auto text-sm underline-offset-4 hover:underline" + // > + // Forgot your password? + // </a> + // <input type="password" placeholder="..." name="password" /> + // </label> + // <button type="submit" className="w-full"> + // Login + // </button> + // <div className="text-center text-sm"> + // Don't have an account?{" "} + // <a href="#" className="underline underline-offset-4"> + // Sign up + // </a> + // </div> + // </form> + ); +} diff --git a/src/components/actiontest.tsx b/src/components/actiontest.tsx new file mode 100644 index 0000000..863f289 --- /dev/null +++ b/src/components/actiontest.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { testFn, testLogin } from "@/actions/test"; +import { useActionState, useState } from "react"; + +export default function TestForm() { + const [state, formAction, isPending] = useActionState<number, FormData>( + testLogin, + 0, + ); + return ( + <form action={formAction}> + <p>State: {state}</p> + <label> + Username + <input type="text" placeholder="shadcn" name="username" /> + </label> + <label className="flex justify-between"> + <span>Password</span> + <input type="password" placeholder="..." name="password" /> + </label> + <button type="submit" className="w-full"> + Login + </button> + </form> + ); +} diff --git a/src/components/counter.tsx b/src/components/counter.tsx index 0e540b8..2122b75 100644 --- a/src/components/counter.tsx +++ b/src/components/counter.tsx @@ -1,11 +1,17 @@ -'use client'; +"use client"; -import { useState } from 'react'; +import { testFn, testLogin } from "@/actions/test"; +import { useState } from "react"; export const Counter = () => { const [count, setCount] = useState(0); - const handleIncrement = () => setCount((c) => c + 1); + const handleIncrement = async () => { + setCount((c) => c + 1); + const res = await testFn("rofl"); + console.log({ res }); + const oof = testLogin({ name: "yago", creds: "xd" }); + }; return ( <section className="border-blue-400 -mx-4 mt-4 rounded-sm border border-dashed p-4"> diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..61875fc --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps<typeof buttonVariants> & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + <Comp + data-slot="button" + className={cn(buttonVariants({ variant, size, className }))} + {...props} + /> + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..3ff199b --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,68 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card" + className={cn( + "bg-card text-card-foreground rounded-xl border shadow-sm", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-header" + className={cn("flex flex-col gap-1.5 p-6", className)} + {...props} + /> + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-title" + className={cn("leading-none font-semibold tracking-tight", className)} + {...props} + /> + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-description" + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-content" + className={cn("p-6 pt-0", className)} + {...props} + /> + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="card-footer" + className={cn("flex items-center p-6 pt-0", className)} + {...props} + /> + ) +} + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..8a83b32 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,165 @@ +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, + useFormState, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +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 } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + 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 +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + <FormItemContext.Provider value={{ id }}> + <div + data-slot="form-item" + className={cn("grid gap-2", className)} + {...props} + /> + </FormItemContext.Provider> + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + const { error, formItemId } = useFormField() + + return ( + <Label + data-slot="form-label" + data-error={!!error} + className={cn("data-[error=true]:text-destructive", className)} + htmlFor={formItemId} + {...props} + /> + ) +} + +function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { + const { error, formItemId, formDescriptionId, formMessageId } = useFormField() + + return ( + <Slot + data-slot="form-control" + id={formItemId} + aria-describedby={ + !error + ? `${formDescriptionId}` + : `${formDescriptionId} ${formMessageId}` + } + aria-invalid={!!error} + {...props} + /> + ) +} + +function FormDescription({ className, ...props }: React.ComponentProps<"p">) { + const { formDescriptionId } = useFormField() + + return ( + <p + data-slot="form-description" + id={formDescriptionId} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> + ) +} + +function FormMessage({ className, ...props }: React.ComponentProps<"p">) { + const { error, formMessageId } = useFormField() + const body = error ? String(error?.message) : props.children + + if (!body) { + return null + } + + return ( + <p + data-slot="form-message" + id={formMessageId} + className={cn("text-destructive text-sm font-medium", className)} + {...props} + > + {body} + </p> + ) +} + +export { + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..596f806 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,19 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + <input + type={type} + data-slot="input" + className={cn( + "border-input file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground aria-invalid:outline-destructive/60 aria-invalid:ring-destructive/20 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 aria-invalid:outline-destructive/60 dark:aria-invalid:outline-destructive dark:aria-invalid:ring-destructive/40 aria-invalid:ring-destructive/20 aria-invalid:border-destructive/60 dark:aria-invalid:border-destructive flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-4 focus-visible:outline-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-[3px] aria-invalid:focus-visible:outline-none md:text-sm dark:aria-invalid:focus-visible:ring-4", + className + )} + {...props} + /> + ) +} + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..f948bc3 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps<typeof LabelPrimitive.Root>) { + return ( + <LabelPrimitive.Root + data-slot="label" + className={cn( + "text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", + className + )} + {...props} + /> + ) +} + +export { Label } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..b624a5b --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,179 @@ +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Root>) { + return <SelectPrimitive.Root data-slot="select" {...props} /> +} + +function SelectGroup({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Group>) { + return <SelectPrimitive.Group data-slot="select-group" {...props} /> +} + +function SelectValue({ + ...props +}: React.ComponentProps<typeof SelectPrimitive.Value>) { + return <SelectPrimitive.Value data-slot="select-value" {...props} /> +} + +function SelectTrigger({ + className, + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Trigger>) { + return ( + <SelectPrimitive.Trigger + data-slot="select-trigger" + className={cn( + "border-input data-[placeholder]:text-muted-foreground aria-invalid:border-destructive ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1", + className + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <ChevronDownIcon className="size-4 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps<typeof SelectPrimitive.Content>) { + return ( + <SelectPrimitive.Portal> + <SelectPrimitive.Content + data-slot="select-content" + className={cn( + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md", + position === "popper" && + "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", + className + )} + position={position} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + "p-1", + position === "popper" && + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Label>) { + return ( + <SelectPrimitive.Label + data-slot="select-label" + className={cn("px-2 py-1.5 text-sm font-semibold", className)} + {...props} + /> + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Item>) { + return ( + <SelectPrimitive.Item + data-slot="select-item" + className={cn( + "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", + className + )} + {...props} + > + <span className="absolute right-2 flex size-3.5 items-center justify-center"> + <SelectPrimitive.ItemIndicator> + <CheckIcon className="size-4" /> + </SelectPrimitive.ItemIndicator> + </span> + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.Separator>) { + return ( + <SelectPrimitive.Separator + data-slot="select-separator" + className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} + {...props} + /> + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) { + return ( + <SelectPrimitive.ScrollUpButton + data-slot="select-scroll-up-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronUpIcon className="size-4" /> + </SelectPrimitive.ScrollUpButton> + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) { + return ( + <SelectPrimitive.ScrollDownButton + data-slot="select-scroll-down-button" + className={cn( + "flex cursor-default items-center justify-center py-1", + className + )} + {...props} + > + <ChevronDownIcon className="size-4" /> + </SelectPrimitive.ScrollDownButton> + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..cd62aff --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,23 @@ +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + <Sonner + theme={theme as ToasterProps["theme"]} + className="toaster group" + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/src/components/ui/spinner.tsx b/src/components/ui/spinner.tsx new file mode 100644 index 0000000..d1511ad --- /dev/null +++ b/src/components/ui/spinner.tsx @@ -0,0 +1,8 @@ +export const Spinner = ({ className }: { className?: string }) => ( + <div + className={ + "w-7 h-7 border-[3px] border-transparent border-t-primary rounded-full animate-spin" + + ` ${className}` + } + /> +); |