From da0524a4fc5fbfb3b87374d5cf374a86943c1c37 Mon Sep 17 00:00:00 2001 From: Taken Date: Thu, 26 Jun 2025 13:46:32 +0200 Subject: [PATCH] Implementr form --- package.json | 2 + pnpm-lock.yaml | 31 + src/app/(admin)/dashboard/page.tsx | 5 +- src/components/dashboard/url-form-card.tsx | 92 +++ src/components/ui/form.tsx | 167 ++++++ src/lib/actions/url.ts | 37 ++ src/lib/db/urls.ts | 15 + .../migrations/0001_flawless_goblin_queen.sql | 2 + .../migrations/meta/0001_snapshot.json | 532 ++++++++++++++++++ src/lib/drizzle/migrations/meta/_journal.json | 7 + src/lib/drizzle/schema.ts | 4 +- src/lib/schema/url.ts | 13 + 12 files changed, 903 insertions(+), 4 deletions(-) create mode 100644 src/components/dashboard/url-form-card.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/lib/actions/url.ts create mode 100644 src/lib/db/urls.ts create mode 100644 src/lib/drizzle/migrations/0001_flawless_goblin_queen.sql create mode 100644 src/lib/drizzle/migrations/meta/0001_snapshot.json create mode 100644 src/lib/schema/url.ts diff --git a/package.json b/package.json index 15575d2..6d6c6a1 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "db:down": "docker compose -f dev-db.yml down" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -33,6 +34,7 @@ "postgres": "^3.4.7", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.58.1", "tailwind-merge": "^3.3.1", "zod": "^3.25.67" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 368cd46..28bbe03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.1.1 + version: 5.1.1(react-hook-form@7.58.1(react@19.1.0)) '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -59,6 +62,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.58.1 + version: 7.58.1(react@19.1.0) tailwind-merge: specifier: ^3.3.1 version: 3.3.1 @@ -478,6 +484,11 @@ packages: '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hookform/resolvers@5.1.1': + resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1100,6 +1111,9 @@ packages: resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} engines: {node: '>=20.0.0'} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2499,6 +2513,12 @@ packages: peerDependencies: react: ^19.1.0 + react-hook-form@7.58.1: + resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3105,6 +3125,11 @@ snapshots: '@hexagon/base64@1.1.28': {} + '@hookform/resolvers@5.1.1(react-hook-form@7.58.1(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.58.1(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3655,6 +3680,8 @@ snapshots: '@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-x509': 2.3.15 + '@standard-schema/utils@0.3.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -5148,6 +5175,10 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-hook-form@7.58.1(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0): diff --git a/src/app/(admin)/dashboard/page.tsx b/src/app/(admin)/dashboard/page.tsx index 749c9d6..977dc87 100644 --- a/src/app/(admin)/dashboard/page.tsx +++ b/src/app/(admin)/dashboard/page.tsx @@ -1,4 +1,5 @@ import { StatsCard } from "@/components/dashboard/stats-card" +import { UrlFormCard } from "@/components/dashboard/url-form-card" import { getSession } from "@/lib/auth/session" import { getDashboardStats } from "@/lib/dashboard/stats" import { LinkIcon, MousePointerClick, TrendingUp } from "lucide-react" @@ -24,8 +25,6 @@ export default async function Dashboard() { return (

Dashboard

-

Welcome to your dashboard! Use the sidebar to navigate to different sections.

-
+ +
) } diff --git a/src/components/dashboard/url-form-card.tsx b/src/components/dashboard/url-form-card.tsx new file mode 100644 index 0000000..1e71693 --- /dev/null +++ b/src/components/dashboard/url-form-card.tsx @@ -0,0 +1,92 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { addUrl } from "@/lib/actions/url" +import { urlFormSchema } from "@/lib/schema/url" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +type UrlFormValues = z.infer + +export function UrlFormCard() { + const form = useForm({ + resolver: zodResolver(urlFormSchema), + defaultValues: { + url: "", + slug: "" + } + }) + + async function handleSubmit(data: UrlFormValues) { + const res = await addUrl(data) + } + + return ( + + + Create Short Link + + Enter a URL and create a custom slug for your short link. + + + +
+ + ( + + Original URL + + + + + The URL you want to shorten. + + + + )} + /> + ( + + Custom Slug + + + + + A unique identifier for your short link. + + + + )} + /> + + + +
+
+ ) +} diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,167 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} 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 = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +