From fc61e5ea9a15b142d90c4b4f20de707046d6f2c9 Mon Sep 17 00:00:00 2001 From: FatahChan Date: Sun, 11 May 2025 17:51:48 +0300 Subject: [PATCH 1/4] feat: blocks --- .vscode/settings.json | 3 +- lefthook.yml | 6 +- package.json | 1 + public/r/basic-info.json | 14 +- public/r/login-form.json | 17 ++ public/r/login.json | 26 ++ public/r/shipping-info.json | 26 ++ public/r/sign-up.json | 26 ++ public/r/tanstack-form.json | 6 - registry.json | 162 ++++++++++--- src/components/block-preview.tsx | 46 ++-- src/components/ui/radio-group.tsx | 43 ++++ src/components/ui/textarea.tsx | 12 +- src/data/blocks.ts | 9 - src/registry/new-york/blocks/basic-info.json | 12 + .../new-york/blocks/{user => }/basic-info.tsx | 3 + src/registry/new-york/blocks/login.json | 11 + src/registry/new-york/blocks/login.tsx | 104 ++++++++ .../new-york/blocks/shipping-info.json | 11 + .../new-york/blocks/shipping-info.tsx | 226 ++++++++++++++++++ src/registry/new-york/blocks/sign-up.json | 11 + src/registry/new-york/blocks/sign-up.tsx | 151 ++++++++++++ src/routeTree.gen.ts | 63 +++-- src/routes/__root.tsx | 2 +- src/routes/blocks/index.tsx | 30 +++ src/routes/index.tsx | 12 +- .../{$category.$slug.tsx => $name.tsx} | 17 +- src/schemas/block-meta-data.ts | 11 + src/schemas/registry-item.ts | 88 +++++++ src/scripts/blocks.ts | 137 +++++++++++ 30 files changed, 1159 insertions(+), 127 deletions(-) create mode 100644 public/r/login-form.json create mode 100644 public/r/login.json create mode 100644 public/r/shipping-info.json create mode 100644 public/r/sign-up.json create mode 100644 src/components/ui/radio-group.tsx delete mode 100644 src/data/blocks.ts create mode 100644 src/registry/new-york/blocks/basic-info.json rename src/registry/new-york/blocks/{user => }/basic-info.tsx (98%) create mode 100644 src/registry/new-york/blocks/login.json create mode 100644 src/registry/new-york/blocks/login.tsx create mode 100644 src/registry/new-york/blocks/shipping-info.json create mode 100644 src/registry/new-york/blocks/shipping-info.tsx create mode 100644 src/registry/new-york/blocks/sign-up.json create mode 100644 src/registry/new-york/blocks/sign-up.tsx create mode 100644 src/routes/blocks/index.tsx rename src/routes/preview/{$category.$slug.tsx => $name.tsx} (52%) create mode 100644 src/schemas/block-meta-data.ts create mode 100644 src/schemas/registry-item.ts create mode 100644 src/scripts/blocks.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3139e12..07f71c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,7 +33,8 @@ "editor.defaultFormatter": "biomejs.biome" }, "editor.codeActionsOnSave": { - "source.organizeImports.biome": "explicit" + "source.organizeImports.biome": "explicit", + "source.fixAll.biome": "explicit" }, "editor.defaultFormatter": "biomejs.biome", "editor.formatOnPaste": true, diff --git a/lefthook.yml b/lefthook.yml index 6addd4b..487006d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -2,9 +2,9 @@ pre-commit: commands: check: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" - run: pnpx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} + run: pnpm biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true registry: - glob: "src/registry/new-york/**/*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" - run: pnpx shadcn@latest build + glob: "registry.json" + run: pnpm shadcn build && git add public/r/* stage_fixed: true diff --git a/package.json b/package.json index 5c364eb..73e46d7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "biome lint", "check": "biome check --fix --unsafe", "registry:build": "shadcn build", + "registry:watch": "tsx watch src/scripts/blocks.ts", "pre-commit": "pnpm run check && pnpm run registry:build" }, "dependencies": { diff --git a/public/r/basic-info.json b/public/r/basic-info.json index 57c17cf..561eebb 100644 --- a/public/r/basic-info.json +++ b/public/r/basic-info.json @@ -5,17 +5,19 @@ "title": "Basic Info", "description": "A basic info form component built with TanStack Form.", "dependencies": [ - "@tanstack/react-form" + "zod" ], "registryDependencies": [ - "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json" + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input", + "textarea" ], "files": [ { - "path": "src/registry/new-york/blocks/user/basic-info.tsx", - "content": "import { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { useCallback } from \"react\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst FormSchema = z.object({\n username: z.string().min(2, {\n message: \"Username must be at least 2 characters.\",\n }),\n email: z.string().email({\n message: \"Please enter a valid email address.\",\n }),\n age: z.number().min(8, {\n message: \"Age must be at least 18 years.\",\n }),\n bio: z.string().max(160, {\n message: \"Bio must not exceed 160 characters.\",\n }),\n});\n\nfunction BasicInfoForm() {\n const form = useAppForm({\n validators: { onBlur: FormSchema },\n defaultValues: {\n username: \"\",\n email: \"\",\n age: 0,\n bio: \"\",\n },\n onSubmit: ({ formApi, value }) => {\n formApi.reset();\n toast.success(Username: {value.username});\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n
\n (\n \n Username\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your public display name.\n \n \n \n )}\n />\n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n Enter your email address for account verification.\n \n \n \n )}\n />\n (\n \n Age\n \n field.handleChange(Number(e.target.value))}\n onBlur={field.handleBlur}\n />\n \n \n Must be at least 18 years old.\n \n \n \n )}\n />\n (\n \n Bio\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n A brief description about yourself (optional).\n \n \n \n )}\n />\n
\n \n \n
\n );\n}\n\nexport default BasicInfoForm;\n", - "type": "registry:block", - "target": "components/forms/user/basic-info.tsx" + "path": "src/registry/new-york/blocks/basic-info.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useCallback } from \"react\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst FormSchema = z.object({\n username: z.string().min(2, {\n message: \"Username must be at least 2 characters.\",\n }),\n email: z.string().email({\n message: \"Please enter a valid email address.\",\n }),\n age: z.number().min(8, {\n message: \"Age must be at least 18 years.\",\n }),\n bio: z.string().max(160, {\n message: \"Bio must not exceed 160 characters.\",\n }),\n});\n\nfunction BasicInfoForm() {\n const form = useAppForm({\n validators: { onBlur: FormSchema },\n defaultValues: {\n username: \"\",\n email: \"\",\n age: 0,\n bio: \"\",\n },\n onSubmit: ({ formApi, value }) => {\n formApi.reset();\n toast.success(Username: {value.username});\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n
\n (\n \n Username\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your public display name.\n \n \n \n )}\n />\n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n Enter your email address for account verification.\n \n \n \n )}\n />\n (\n \n Age\n \n field.handleChange(Number(e.target.value))}\n onBlur={field.handleBlur}\n />\n \n \n Must be at least 18 years old.\n \n \n \n )}\n />\n (\n \n Bio\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n A brief description about yourself (optional).\n \n \n \n )}\n />\n
\n \n \n
\n );\n}\n\nBasicInfoForm.displayName = \"BasicInfoForm\";\n\nexport default BasicInfoForm;\n", + "type": "registry:block" } ], "categories": [ diff --git a/public/r/login-form.json b/public/r/login-form.json new file mode 100644 index 0000000..4da5c11 --- /dev/null +++ b/public/r/login-form.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "login-form", + "type": "registry:block", + "title": "Login Form", + "description": "A login form built with TanStack Form.", + "registryDependencies": [ + "https://fatahchan.github.io/shadcn-tanstack-form/r/tanstack-form.json" + ], + "files": [ + { + "path": "src/registry/new-york/blocks/auth/login-form.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { cn } from \"@/lib/utils\";\nimport {\n type ComponentProps,\n type FormHTMLAttributes,\n useCallback,\n} from \"react\";\nimport * as z from \"zod\";\n\nconst loginFormSchema = z.object({\n email: z.string(),\n password: z.string().min(1),\n});\n\ninterface LoginFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nexport default function LoginForm({\n onSubmit,\n defaultValues,\n className,\n ...props\n}: LoginFormProps) {\n const form = useAppForm({\n defaultValues: {\n email: defaultValues?.email ?? \"\",\n password: defaultValues?.password ?? \"\",\n },\n validators: { onBlur: loginFormSchema },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n This is your email\n \n \n )}\n />\n\n (\n \n Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your password\n \n \n \n )}\n />\n \n \n \n );\n}\n", + "type": "registry:block" + } + ] +} \ No newline at end of file diff --git a/public/r/login.json b/public/r/login.json new file mode 100644 index 0000000..d6ace19 --- /dev/null +++ b/public/r/login.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "login", + "type": "registry:block", + "title": "Login", + "description": "A login form component built with TanStack Form.", + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ], + "files": [ + { + "path": "src/registry/new-york/blocks/login.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { cn } from \"@/lib/utils\";\nimport { type FormHTMLAttributes, useCallback } from \"react\";\nimport * as z from \"zod\";\n\nconst loginFormSchema = z.object({\n email: z.string(),\n password: z.string().min(1),\n});\n\ninterface LoginFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction LoginForm({\n onSubmit,\n\n defaultValues,\n className,\n ...props\n}: LoginFormProps) {\n const form = useAppForm({\n defaultValues: {\n email: defaultValues?.email ?? \"\",\n password: defaultValues?.password ?? \"\",\n },\n validators: { onBlur: loginFormSchema },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n This is your email\n \n \n )}\n />\n\n (\n \n Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your password\n \n \n \n )}\n />\n \n \n \n );\n}\n\nLoginForm.displayName = \"LoginForm\";\n\nexport default LoginForm;\n", + "type": "registry:block" + } + ], + "categories": [ + "forms", + "auth" + ] +} \ No newline at end of file diff --git a/public/r/shipping-info.json b/public/r/shipping-info.json new file mode 100644 index 0000000..23f0c25 --- /dev/null +++ b/public/r/shipping-info.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "shipping-info", + "type": "registry:block", + "title": "Shipping Info", + "description": "A shipping information form component built with TanStack Form.", + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ], + "files": [ + { + "path": "src/registry/new-york/blocks/shipping-info.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { useCallback } from \"react\";\nimport type { FormHTMLAttributes } from \"react\";\nimport { z } from \"zod\";\n\nconst shippingSchema = z.object({\n fullName: z.string().min(2, {\n message: \"Full name must be at least 2 characters long.\",\n }),\n addressLine1: z.string().min(5, {\n message: \"Address must be at least 5 characters long.\",\n }),\n addressLine2: z.string(),\n city: z.string().min(2, {\n message: \"City must be at least 2 characters long.\",\n }),\n state: z.string().min(2, {\n message: \"State must be at least 2 characters long.\",\n }),\n postalCode: z.string().min(4, {\n message: \"Please enter a valid postal code.\",\n }),\n phone: z.string().min(8, {\n message: \"Please enter a valid phone number.\",\n }),\n});\n\ninterface ShippingFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction ShippingForm({\n onSubmit,\n defaultValues,\n className,\n ...props\n}: ShippingFormProps) {\n const form = useAppForm({\n defaultValues: {\n fullName: defaultValues?.fullName ?? \"\",\n addressLine1: defaultValues?.addressLine1 ?? \"\",\n addressLine2: defaultValues?.addressLine2 ?? \"\",\n city: defaultValues?.city ?? \"\",\n state: defaultValues?.state ?? \"\",\n postalCode: defaultValues?.postalCode ?? \"\",\n phone: defaultValues?.phone ?? \"\",\n },\n validators: { onBlur: shippingSchema },\n onSubmit: ({ value }) => {\n onSubmit(value);\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n\n return (\n \n \n (\n \n Full Name\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Address Line 1\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Address Line 2 (Optional)\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n
\n (\n \n City\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n State\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n
\n\n
\n (\n \n Postal Code\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Phone Number\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n
\n\n \n \n
\n );\n}\n\nShippingForm.displayName = \"ShippingForm\";\nShippingForm.__CATEGORIES = [\"forms\", \"checkout\"];\nShippingForm.__TITLE = \"Shipping Info\";\nShippingForm.__DESCRIPTION =\n \"A shipping information form component built with TanStack Form.\";\nShippingForm.__DEPENDENCIES = [\"zod\"];\nShippingForm.__REGISTRY_DEPENDENCIES = [\n \"https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json\",\n \"button\",\n \"input\",\n];\n\nexport default ShippingForm;\n", + "type": "registry:block" + } + ], + "categories": [ + "forms", + "checkout" + ] +} \ No newline at end of file diff --git a/public/r/sign-up.json b/public/r/sign-up.json new file mode 100644 index 0000000..1b42db8 --- /dev/null +++ b/public/r/sign-up.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "sign-up", + "type": "registry:block", + "title": "Sign Up", + "description": "A sign up form component built with TanStack Form.", + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ], + "files": [ + { + "path": "src/registry/new-york/blocks/sign-up.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { useCallback } from \"react\";\nimport type { FormHTMLAttributes } from \"react\";\nimport { z } from \"zod\";\n\nconst signUpSchema = z\n .object({\n email: z.string().email({\n message: \"Please enter a valid email address.\",\n }),\n password: z.string().min(8, {\n message: \"Password must be at least 8 characters long.\",\n }),\n confirmPassword: z.string(),\n fullName: z.string().min(2, {\n message: \"Full name must be at least 2 characters long.\",\n }),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: \"Passwords do not match\",\n path: [\"confirmPassword\"],\n });\n\ninterface SignUpFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction SignUpForm({\n onSubmit,\n defaultValues,\n className,\n ...props\n}: SignUpFormProps) {\n const form = useAppForm({\n defaultValues: {\n email: defaultValues?.email ?? \"\",\n password: defaultValues?.password ?? \"\",\n confirmPassword: defaultValues?.confirmPassword ?? \"\",\n fullName: defaultValues?.fullName ?? \"\",\n },\n validators: { onBlur: signUpSchema },\n onSubmit: ({ value }) => {\n onSubmit(value);\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n\n return (\n \n \n (\n \n Full Name\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Confirm Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n \n \n \n );\n}\n\nSignUpForm.displayName = \"SignUpForm\";\n\nexport default SignUpForm;\n", + "type": "registry:block" + } + ], + "categories": [ + "forms", + "auth" + ] +} \ No newline at end of file diff --git a/public/r/tanstack-form.json b/public/r/tanstack-form.json index 51ee118..0aeffd8 100644 --- a/public/r/tanstack-form.json +++ b/public/r/tanstack-form.json @@ -4,12 +4,6 @@ "type": "registry:ui", "title": "TanStack Form", "description": "A form component built with TanStack Form.", - "dependencies": [ - "@tanstack/react-form" - ], - "registryDependencies": [ - "label" - ], "files": [ { "path": "src/registry/new-york/tanstack-form/tanstack-form.tsx", diff --git a/registry.json b/registry.json index fc3f4ae..33b735f 100644 --- a/registry.json +++ b/registry.json @@ -1,37 +1,129 @@ { - "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "shadcn-tanstack-form", - "homepage": "https://fatahchan.github.io/shadcn-tanstack-form/", - "items": [ + "$schema": "https://ui.shadcn.com/schema/registry.json", + "name": "shadcn-tanstack-form", + "homepage": "https://fatahchan.github.io/shadcn-tanstack-form/", + "items": [ + { + "name": "tanstack-form", + "type": "registry:ui", + "title": "TanStack Form", + "description": "A form component built with TanStack Form.", + "files": [ { - "name": "tanstack-form", - "type": "registry:ui", - "title": "TanStack Form", - "description": "A form component built with TanStack Form.", - "files": [ - { - "path": "src/registry/new-york/tanstack-form/tanstack-form.tsx", - "type": "registry:ui" - } - ], - "dependencies": ["@tanstack/react-form"], - "registryDependencies": ["label"] - }, - { - "name": "basic-info", - "type": "registry:block", - "title": "Basic Info", - "description": "A basic info form component built with TanStack Form.", - "categories": ["forms", "user"], - "files": [ - { - "path": "src/registry/new-york/blocks/user/basic-info.tsx", - "type": "registry:block", - "target": "components/forms/user/basic-info.tsx" - } - ], - "registryDependencies": ["https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", "textarea"] - } - ] - } - \ No newline at end of file + "name": "tanstack-form", + "type": "registry:ui", + "title": "TanStack Form", + "path": "src/registry/new-york/tanstack-form/tanstack-form.tsx", + "description": "A form component built with TanStack Form.", + "dependencies": [ + "@tanstack/react-form" + ], + "registryDependencies": [ + "label" + ] + } + ] + }, + { + "name": "basic-info", + "type": "registry:block", + "title": "Basic Info", + "description": "A basic info form component built with TanStack Form.", + "files": [ + { + "type": "registry:block", + "path": "src/registry/new-york/blocks/basic-info.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useCallback } from \"react\";\nimport { toast } from \"sonner\";\nimport { z } from \"zod\";\n\nconst FormSchema = z.object({\n username: z.string().min(2, {\n message: \"Username must be at least 2 characters.\",\n }),\n email: z.string().email({\n message: \"Please enter a valid email address.\",\n }),\n age: z.number().min(8, {\n message: \"Age must be at least 18 years.\",\n }),\n bio: z.string().max(160, {\n message: \"Bio must not exceed 160 characters.\",\n }),\n});\n\nfunction BasicInfoForm() {\n const form = useAppForm({\n validators: { onBlur: FormSchema },\n defaultValues: {\n username: \"\",\n email: \"\",\n age: 0,\n bio: \"\",\n },\n onSubmit: ({ formApi, value }) => {\n formApi.reset();\n toast.success(Username: {value.username});\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n
\n (\n \n Username\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your public display name.\n \n \n \n )}\n />\n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n Enter your email address for account verification.\n \n \n \n )}\n />\n (\n \n Age\n \n field.handleChange(Number(e.target.value))}\n onBlur={field.handleBlur}\n />\n \n \n Must be at least 18 years old.\n \n \n \n )}\n />\n (\n \n Bio\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n A brief description about yourself (optional).\n \n \n \n )}\n />\n
\n \n \n
\n );\n}\n\nBasicInfoForm.displayName = \"BasicInfoForm\";\n\nexport default BasicInfoForm;\n" + } + ], + "categories": [ + "forms", + "user" + ], + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input", + "textarea" + ] + }, + { + "name": "login", + "type": "registry:block", + "title": "Login", + "description": "A login form component built with TanStack Form.", + "files": [ + { + "type": "registry:block", + "path": "src/registry/new-york/blocks/login.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { cn } from \"@/lib/utils\";\nimport { type FormHTMLAttributes, useCallback } from \"react\";\nimport * as z from \"zod\";\n\nconst loginFormSchema = z.object({\n email: z.string(),\n password: z.string().min(1),\n});\n\ninterface LoginFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction LoginForm({\n onSubmit,\n\n defaultValues,\n className,\n ...props\n}: LoginFormProps) {\n const form = useAppForm({\n defaultValues: {\n email: defaultValues?.email ?? \"\",\n password: defaultValues?.password ?? \"\",\n },\n validators: { onBlur: loginFormSchema },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n return (\n \n \n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n This is your email\n \n \n )}\n />\n\n (\n \n Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n This is your password\n \n \n \n )}\n />\n \n \n \n );\n}\n\nLoginForm.displayName = \"LoginForm\";\n\nexport default LoginForm;\n" + } + ], + "categories": [ + "forms", + "auth" + ], + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ] + }, + { + "name": "shipping-info", + "type": "registry:block", + "title": "Shipping Info", + "description": "A shipping information form component built with TanStack Form.", + "files": [ + { + "type": "registry:block", + "path": "src/registry/new-york/blocks/shipping-info.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { useCallback } from \"react\";\nimport type { FormHTMLAttributes } from \"react\";\nimport { z } from \"zod\";\n\nconst shippingSchema = z.object({\n fullName: z.string().min(2, {\n message: \"Full name must be at least 2 characters long.\",\n }),\n addressLine1: z.string().min(5, {\n message: \"Address must be at least 5 characters long.\",\n }),\n addressLine2: z.string(),\n city: z.string().min(2, {\n message: \"City must be at least 2 characters long.\",\n }),\n state: z.string().min(2, {\n message: \"State must be at least 2 characters long.\",\n }),\n postalCode: z.string().min(4, {\n message: \"Please enter a valid postal code.\",\n }),\n phone: z.string().min(8, {\n message: \"Please enter a valid phone number.\",\n }),\n});\n\ninterface ShippingFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction ShippingForm({\n onSubmit,\n defaultValues,\n className,\n ...props\n}: ShippingFormProps) {\n const form = useAppForm({\n defaultValues: {\n fullName: defaultValues?.fullName ?? \"\",\n addressLine1: defaultValues?.addressLine1 ?? \"\",\n addressLine2: defaultValues?.addressLine2 ?? \"\",\n city: defaultValues?.city ?? \"\",\n state: defaultValues?.state ?? \"\",\n postalCode: defaultValues?.postalCode ?? \"\",\n phone: defaultValues?.phone ?? \"\",\n },\n validators: { onBlur: shippingSchema },\n onSubmit: ({ value }) => {\n onSubmit(value);\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n\n return (\n \n \n (\n \n Full Name\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Address Line 1\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Address Line 2 (Optional)\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n
\n (\n \n City\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n State\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n
\n\n
\n (\n \n Postal Code\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Phone Number\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n
\n\n \n \n
\n );\n}\n\nShippingForm.displayName = \"ShippingForm\";\nShippingForm.__CATEGORIES = [\"forms\", \"checkout\"];\nShippingForm.__TITLE = \"Shipping Info\";\nShippingForm.__DESCRIPTION =\n \"A shipping information form component built with TanStack Form.\";\nShippingForm.__DEPENDENCIES = [\"zod\"];\nShippingForm.__REGISTRY_DEPENDENCIES = [\n \"https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json\",\n \"button\",\n \"input\",\n];\n\nexport default ShippingForm;\n" + } + ], + "categories": [ + "forms", + "checkout" + ], + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ] + }, + { + "name": "sign-up", + "type": "registry:block", + "title": "Sign Up", + "description": "A sign up form component built with TanStack Form.", + "files": [ + { + "type": "registry:block", + "path": "src/registry/new-york/blocks/sign-up.tsx", + "content": "\"use client\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAppForm } from \"@/components/ui/tanstack-form\";\nimport { useCallback } from \"react\";\nimport type { FormHTMLAttributes } from \"react\";\nimport { z } from \"zod\";\n\nconst signUpSchema = z\n .object({\n email: z.string().email({\n message: \"Please enter a valid email address.\",\n }),\n password: z.string().min(8, {\n message: \"Password must be at least 8 characters long.\",\n }),\n confirmPassword: z.string(),\n fullName: z.string().min(2, {\n message: \"Full name must be at least 2 characters long.\",\n }),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: \"Passwords do not match\",\n path: [\"confirmPassword\"],\n });\n\ninterface SignUpFormProps\n extends Omit, \"onSubmit\"> {\n onSubmit: (data: z.infer) => void;\n defaultValues?: z.infer;\n}\n\nfunction SignUpForm({\n onSubmit,\n defaultValues,\n className,\n ...props\n}: SignUpFormProps) {\n const form = useAppForm({\n defaultValues: {\n email: defaultValues?.email ?? \"\",\n password: defaultValues?.password ?? \"\",\n confirmPassword: defaultValues?.confirmPassword ?? \"\",\n fullName: defaultValues?.fullName ?? \"\",\n },\n validators: { onBlur: signUpSchema },\n onSubmit: ({ value }) => {\n onSubmit(value);\n },\n });\n\n const handleSubmit = useCallback(\n (e: React.FormEvent) => {\n e.preventDefault();\n e.stopPropagation();\n form.handleSubmit();\n },\n [form],\n );\n\n return (\n \n \n (\n \n Full Name\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Email\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n (\n \n Confirm Password\n \n field.handleChange(e.target.value)}\n onBlur={field.handleBlur}\n />\n \n \n \n )}\n />\n\n \n \n \n );\n}\n\nSignUpForm.displayName = \"SignUpForm\";\n\nexport default SignUpForm;\n" + } + ], + "categories": [ + "forms", + "auth" + ], + "dependencies": [ + "zod" + ], + "registryDependencies": [ + "https://shadcn-tanstack-form.netlify.app/r/tanstack-form.json", + "button", + "input" + ] + } + ] +} \ No newline at end of file diff --git a/src/components/block-preview.tsx b/src/components/block-preview.tsx index 94b63e4..ccd6af4 100644 --- a/src/components/block-preview.tsx +++ b/src/components/block-preview.tsx @@ -7,6 +7,7 @@ import { import { useIframeHeight } from "@/hooks/use-iframe-height"; import { usePreload } from "@/hooks/use-preload"; import { cn } from "@/lib/utils"; +import type { RegistryItem } from "@/schemas/registry-item"; import * as RadioGroup from "@radix-ui/react-radio-group"; import { Link } from "@tanstack/react-router"; import { Check, Code2, Copy, Eye, Maximize, Terminal } from "lucide-react"; @@ -24,14 +25,7 @@ import { Button } from "./ui/button"; const Iframe = lazy(() => import("./iframe").then((module) => ({ default: module.Iframe })), ); -type Block = { - code?: string; - preview: string; - title?: string; - category?: string; - previewOnly?: boolean; - slug: string; -}; + const radioItem = "rounded-(--radius) duration-200 flex items-center justify-center h-8 px-2.5 gap-2 transition-[color] data-[state=checked]:bg-muted"; @@ -42,19 +36,20 @@ const LGSIZE = 82; const DEFAULT_HEIGHT = 224; -export const BlockPreview: React.FC = ({ - code, - preview, - title, - category, - previewOnly, - slug, -}) => { +export const BlockPreview: React.FC< + RegistryItem & { previewOnly?: boolean } +> = ({ previewOnly, ...props }) => { + const { name, files, title } = props; + const content = + files?.find((file) => file.type === "registry:block")?.content || ""; + + const preview = `/preview/${name}`; + const [width, setWidth] = useState(DEFAULTSIZE); const [mode, setMode] = useState<"preview" | "code">("preview"); - const { iframeHeight, measureRef } = useIframeHeight(preview, DEFAULT_HEIGHT); + const { iframeHeight, measureRef } = useIframeHeight(name, DEFAULT_HEIGHT); - const terminalCode = `pnpm dlx shadcn@canary add https://shadcn-tanstack-form.netlify.app/r/${slug}.json`; + const terminalCode = `pnpm dlx shadcn@canary add https://shadcn-tanstack-form.netlify.app/r/${name}.json`; const [copied, copy] = useCopyToClipboard(); const [cliCopied, cliCopy] = useCopyToClipboard(); @@ -67,7 +62,6 @@ export const BlockPreview: React.FC = ({ rootMargin: "0px", }); const shouldLoadIframe = entry?.isIntersecting ?? false; - console.log(shouldLoadIframe); usePreload({ link: preview, @@ -87,7 +81,7 @@ export const BlockPreview: React.FC = ({
- {code && ( + {content && ( <> = ({
- {code && ( + {content && ( <>