diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9cb02802a..0a13a63ca 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ - i18n: Use `next-intl`, `t('key')` for translations, `getTranslation` (server), `useTranslation` (client) - Accessibility: WCAG 2.1 AA, semantic HTML, ARIA, keyboard/screen reader support - **Backend:** - - Hono.js 4, OpenAPI via `@hono/zod-openapi`, Zod 3 for validation + - Hono.js 4, OpenAPI via `@hono/zod-openapi`, Zod 4 for validation - Database: PostgreSQL via Drizzle ORM, access via `c.get('database')` - API: RESTful, versioned, rate-limited, secure session management - Error handling: Use Hono's error middleware, log via `c.get('log')` diff --git a/.github/docs/prd.md b/.github/docs/prd.md index 2fa4f21ae..727839c0c 100644 --- a/.github/docs/prd.md +++ b/.github/docs/prd.md @@ -150,7 +150,7 @@ VitNode is designed for individual developers and small teams who need a structu - React 19 with Server Components - TypeScript 5 with strict configuration - Tailwind CSS 4 with Shadcn UI components -- Zod 3 for runtime validation +- Zod 4 for runtime validation - React Hook Form 7 for form management - Next-intl for internationalization diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 8f599dfd2..1e73d61c5 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -22,7 +22,7 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 10.12.4 + version: 10.13.1 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/bump_publish.yml b/.github/workflows/bump_publish.yml index f9691e4f4..01eb480cc 100644 --- a/.github/workflows/bump_publish.yml +++ b/.github/workflows/bump_publish.yml @@ -52,7 +52,7 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 10.12.4 + version: 10.13.1 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/README.md b/README.md index 36acaa713..193a0c5d9 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ - **Enhanced Plugin System**: Improved CLI tools for plugins - **Better Documentation**: Completely rewritten docs and website - **Streamlined Configuration**: Single config file for all settings +- **Zod 4**: Upgraded to the latest version for schema validation ## 🔍 Project Scope diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs index dd3efc12a..c1de0a4ee 100644 --- a/apps/api/eslint.config.mjs +++ b/apps/api/eslint.config.mjs @@ -1,8 +1,20 @@ import eslintVitNode from '@vitnode/eslint-config/eslint'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default [ ...eslintVitNode, { ignores: ['drizzle.config.ts'], }, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + }, ]; diff --git a/apps/api/package.json b/apps/api/package.json index 136a0edbc..d963e3b8f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,27 +15,27 @@ "drizzle-kit": "drizzle-kit" }, "dependencies": { - "@hono/zod-openapi": "^0.19.9", - "@hono/zod-validator": "^0.7.0", - "@react-email/components": "^0.2.0", + "@hono/zod-openapi": "^1.0.2", + "@hono/zod-validator": "^0.7.2", + "@react-email/components": "^0.3.2", "@vitnode/core": "workspace:*", "drizzle-kit": "^0.31.4", - "drizzle-orm": "^0.44.2", - "hono": "^4.8.4", + "drizzle-orm": "^0.44.3", + "hono": "^4.8.5", "next-intl": "^4.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", - "zod": "^3.25.76" + "zod": "^4.0.5" }, "devDependencies": { - "@hono/node-server": "^1.15.0", - "@types/node": "^24.0.12", + "@hono/node-server": "^1.17.1", + "@types/node": "^24.1.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitnode/eslint-config": "workspace:*", "dotenv": "^17.2.0", - "eslint": "^9.30.1", - "react-email": "^4.1.1", + "eslint": "^9.31.0", + "react-email": "^4.2.3", "tsc-alias": "^1.8.16", "tsx": "^4.20.3", "typescript": "^5.8.3" diff --git a/apps/docs/content/docs/ui/auto-form.mdx b/apps/docs/content/docs/ui/auto-form.mdx index e4d098759..bfe6cf10b 100644 --- a/apps/docs/content/docs/ui/auto-form.mdx +++ b/apps/docs/content/docs/ui/auto-form.mdx @@ -21,7 +21,9 @@ import { z } from 'zod'; ```ts const formSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), - email: z.string().email('Please enter a valid email address'), + email: z + .email('Please enter a valid email address') + .describe("We'll use this email to contact you. (from zod schema)"), user_type: z.enum(['admin', 'editor', 'viewer']), accept_terms: z.boolean().refine(val => val, { message: 'You must accept the terms and conditions', @@ -37,27 +39,23 @@ const formSchema = z.object({ id: 'username', component: props => ( ), }, { id: 'email', component: props => ( - + ), }, { id: 'user_type', component: props => ( ), }, @@ -73,8 +70,8 @@ const formSchema = z.object({ id: 'accept_terms', component: props => ( ), }, @@ -82,10 +79,10 @@ const formSchema = z.object({ id: 'description', component: props => ( ), }, @@ -127,7 +124,7 @@ Auto Form supports all Zod validators: ```ts const formSchema = z.object({ username: z.string().min(3).max(20), - email: z.string().email(), + email: z.email(), age: z.number().min(18).max(120), password: z .string() @@ -138,6 +135,12 @@ const formSchema = z.object({ }); ``` +## Custom Fields + + + We're working hard to bring you the best documentation experience. + + ## Form Submission To activate submit button and handle form submission with the `onSubmit` callback: @@ -149,9 +152,9 @@ To activate submit button and handle form submission with the `onSubmit` callbac id: 'username', component: props => ( ), }, diff --git a/apps/docs/content/docs/ui/checkbox.mdx b/apps/docs/content/docs/ui/checkbox.mdx index d65f5914d..3226157a3 100644 --- a/apps/docs/content/docs/ui/checkbox.mdx +++ b/apps/docs/content/docs/ui/checkbox.mdx @@ -36,8 +36,8 @@ const formSchema = z.object({ id: 'acceptTerms', component: props => ( ), }, diff --git a/apps/docs/content/docs/ui/combobox-async.mdx b/apps/docs/content/docs/ui/combobox-async.mdx index ae9602dad..4514add41 100644 --- a/apps/docs/content/docs/ui/combobox-async.mdx +++ b/apps/docs/content/docs/ui/combobox-async.mdx @@ -32,6 +32,7 @@ const formSchema = z.object({ id: 'categoryId', component: props => ( { const res = await fetcherClient(categoriesModule, { path: '/', @@ -52,7 +53,6 @@ const formSchema = z.object({ }} id="categoryId" label="Category" - {...props} /> ), }, diff --git a/apps/docs/content/docs/ui/combobox.mdx b/apps/docs/content/docs/ui/combobox.mdx index a39520d3c..3190414cc 100644 --- a/apps/docs/content/docs/ui/combobox.mdx +++ b/apps/docs/content/docs/ui/combobox.mdx @@ -34,6 +34,7 @@ const formSchema = z.object({ id: 'type', component: props => ( ), }, diff --git a/apps/docs/content/docs/ui/input.mdx b/apps/docs/content/docs/ui/input.mdx index 57dc50e7a..78193dd0a 100644 --- a/apps/docs/content/docs/ui/input.mdx +++ b/apps/docs/content/docs/ui/input.mdx @@ -22,7 +22,7 @@ import { AutoFormInput } from '@vitnode/core/components/form/fields/input'; ```ts const formSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), - email: z.string().email('Please enter a valid email address'), + email: z.email('Please enter a valid email address'), }); ``` @@ -34,9 +34,9 @@ const formSchema = z.object({ id: 'username', component: props => ( ), }, @@ -44,10 +44,9 @@ const formSchema = z.object({ id: 'email', component: props => ( ), }, diff --git a/apps/docs/content/docs/ui/radio-group.mdx b/apps/docs/content/docs/ui/radio-group.mdx index 41da0ac95..2b9368ad8 100644 --- a/apps/docs/content/docs/ui/radio-group.mdx +++ b/apps/docs/content/docs/ui/radio-group.mdx @@ -33,6 +33,7 @@ const formSchema = z.object({ id: 'options', component: props => ( ), }, diff --git a/apps/docs/content/docs/ui/switch.mdx b/apps/docs/content/docs/ui/switch.mdx index 398cb7b7f..412ca7bb4 100644 --- a/apps/docs/content/docs/ui/switch.mdx +++ b/apps/docs/content/docs/ui/switch.mdx @@ -35,7 +35,7 @@ const formSchema = z.object({ { id: 'acceptTerms', component: props => ( - + ), }, ]} diff --git a/apps/docs/content/docs/ui/textarea.mdx b/apps/docs/content/docs/ui/textarea.mdx index a4855470b..42559f631 100644 --- a/apps/docs/content/docs/ui/textarea.mdx +++ b/apps/docs/content/docs/ui/textarea.mdx @@ -34,10 +34,10 @@ const formSchema = z.object({ id: 'desc', component: props => ( ), }, diff --git a/apps/docs/eslint.config.mjs b/apps/docs/eslint.config.mjs index f6c3de516..f049e77eb 100644 --- a/apps/docs/eslint.config.mjs +++ b/apps/docs/eslint.config.mjs @@ -1,8 +1,20 @@ import eslintVitNode from '@vitnode/eslint-config/eslint'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default [ ...eslintVitNode, { ignores: ['.source'], }, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + }, ]; diff --git a/apps/docs/package.json b/apps/docs/package.json index 0fd0b8b98..597fc94f4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,7 +9,7 @@ "init": "vitnode init", "dev": "vitnode init && next dev --turbopack", "dev:email": "email dev --dir src/emails", - "build": "next build --turbopack", + "build": "next build", "start": "next start", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -21,20 +21,20 @@ "drizzle-kit": "drizzle-kit" }, "dependencies": { - "@hono/zod-openapi": "^0.19.9", - "@hono/zod-validator": "^0.7.0", + "@hono/zod-openapi": "^1.0.2", + "@hono/zod-validator": "^0.7.2", "@vitnode/blog": "workspace:*", "@vitnode/core": "workspace:*", "babel-plugin-react-compiler": "19.1.0-rc.2", "drizzle-kit": "^0.31.4", - "drizzle-orm": "^0.44.2", - "fumadocs-core": "^15.6.3", - "fumadocs-mdx": "^11.6.10", - "fumadocs-ui": "^15.6.3", - "hono": "^4.8.4", + "drizzle-orm": "^0.44.3", + "fumadocs-core": "^15.6.5", + "fumadocs-mdx": "^11.7.0", + "fumadocs-ui": "^15.6.5", + "hono": "^4.8.5", "lucide-react": "^0.525.0", - "motion": "^12.23.3", - "next": "^15.3.5", + "motion": "^12.23.6", + "next": "^15.4.2", "next-intl": "^4.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -43,21 +43,21 @@ "sonner": "^2.0.6" }, "devDependencies": { - "@playwright/test": "^1.54.0", + "@playwright/test": "^1.54.1", "@tailwindcss/postcss": "^4.1.11", "@types/mdx": "^2.0.13", - "@types/node": "^24.0.12", + "@types/node": "^24.1.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitnode/eslint-config": "workspace:*", "class-variance-authority": "^0.7.1", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "postcss": "^8.5.6", - "react-email": "^4.1.1", - "shiki": "^3.7.0", + "react-email": "^4.2.3", + "shiki": "^3.8.1", "tailwindcss": "^4.1.11", "tw-animate-css": "^1.3.5", "typescript": "^5.8.3", - "zod": "^3.25.76" + "zod": "^4.0.5" } } diff --git a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx index 514e6afb9..705a91568 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx @@ -3,7 +3,9 @@ import type { Metadata } from 'next'; import { buttonVariants } from '@vitnode/core/components/ui/button'; import { cn } from '@vitnode/core/lib/utils'; import Link from 'fumadocs-core/link'; +import { ChevronRight } from 'lucide-react'; +import { AnimatedBeamHome } from '../../../../components/animated-beam/animated-beam-home'; import { AdminSection } from './sections/admin/admin'; import { CallToActionSection } from './sections/call-to-action'; import { PoweringBySection } from './sections/powering-by/powering-by'; @@ -17,17 +19,8 @@ export const metadata: Metadata = { export default function HomePage() { return (
-
+
- -
- 🎉{` `} - VitNode 2.0 in progress... - {/* */} - -

Extendable Framework for Building Apps @@ -47,15 +40,16 @@ export default function HomePage() { )} href="/docs/dev" > - Get Started + Get Started

-
Here will be some img or something else
+
something
+
diff --git a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx index 1aef86579..c3e345f88 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/sections/powering-by/powering-by.tsx @@ -12,7 +12,7 @@ import { TurboRepoLogo } from './logos/turborepo'; export const PoweringBySection = () => { return ( -
+
diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index 824d0ae7a..a32f861e8 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -54,7 +54,7 @@ --card-foreground: oklch(0.96 0.01 250); --popover: oklch(0.22 0.01 250); --popover-foreground: oklch(0.96 0.01 250); - --primary: oklch(0.51 0.16 262.61); + --primary: oklch(0.6 0.18 262.65); --primary-foreground: oklch(0.98 0 0); --secondary: oklch(0.29 0.03 264.9); --secondary-foreground: oklch(0.96 0.01 250); diff --git a/apps/docs/src/components/animated-beam/animated-beam-home.tsx b/apps/docs/src/components/animated-beam/animated-beam-home.tsx new file mode 100644 index 000000000..7de512a8a --- /dev/null +++ b/apps/docs/src/components/animated-beam/animated-beam-home.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@vitnode/core/components/ui/tooltip'; +import { Link } from '@vitnode/core/lib/navigation'; +import { cn } from '@vitnode/core/lib/utils'; +import { + AtSign, + Database, + Languages, + Paintbrush, + Plug, + ShieldCheck, + Sparkle, + Users, +} from 'lucide-react'; +import React, { useRef } from 'react'; + +import { LogoVitNode } from '../logo-vitnode'; +import { AnimatedBeam } from './animated-beam'; + +const Circle = ({ + className, + tooltip, + ...props +}: React.ComponentProps & { + tooltip?: string; +}) => { + const classNameLink = cn( + 'bg-card hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 z-10 flex size-12 items-center justify-center rounded-md border p-3 transition-all focus-visible:ring-[3px]', + className, + ); + + if (!tooltip) { + return ; + } + + return ( + + + + + + + {tooltip} + + + ); +}; + +Circle.displayName = 'Circle'; + +export function AnimatedBeamHome() { + const containerRef = useRef(null); + const div1Ref = useRef(null); + const div2Ref = useRef(null); + const div3Ref = useRef(null); + const div4Ref = useRef(null); + const div5Ref = useRef(null); + const div6Ref = useRef(null); + const div7Ref = useRef(null); + const div8Ref = useRef(null); + const div9Ref = useRef(null); + + return ( +
+
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/animated-beam/animated-beam.tsx b/apps/docs/src/components/animated-beam/animated-beam.tsx new file mode 100644 index 000000000..074f22b95 --- /dev/null +++ b/apps/docs/src/components/animated-beam/animated-beam.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { cn } from '@vitnode/core/lib/utils'; +import { motion } from 'motion/react'; +import { type RefObject, useEffect, useId, useState } from 'react'; + +export const AnimatedBeam = ({ + className, + containerRef, + fromRef, + toRef, + curvature = 0, + reverse = false, // Include the reverse prop + duration = Math.random() * 3 + 4, + delay = 0, + pathColor = 'gray', + pathWidth = 2, + pathOpacity = 0.2, + gradientStartColor = '#325fbd', + gradientStopColor = '#363895', + startXOffset = 0, + startYOffset = 0, + endXOffset = 0, + endYOffset = 0, +}: { + className?: string; + containerRef: RefObject; // Container ref + curvature?: number; + delay?: number; + duration?: number; + endXOffset?: number; + endYOffset?: number; + fromRef: RefObject; + gradientStartColor?: string; + gradientStopColor?: string; + pathColor?: string; + pathOpacity?: number; + pathWidth?: number; + reverse?: boolean; + startXOffset?: number; + startYOffset?: number; + toRef: RefObject; +}) => { + const id = useId(); + const [pathD, setPathD] = useState(''); + const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }); + + // Calculate the gradient coordinates based on the reverse prop + const gradientCoordinates = reverse + ? { + x1: ['90%', '-10%'], + x2: ['100%', '0%'], + y1: ['0%', '0%'], + y2: ['0%', '0%'], + } + : { + x1: ['10%', '110%'], + x2: ['0%', '100%'], + y1: ['0%', '0%'], + y2: ['0%', '0%'], + }; + + useEffect(() => { + const updatePath = () => { + if (containerRef.current && fromRef.current && toRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const rectA = fromRef.current.getBoundingClientRect(); + const rectB = toRef.current.getBoundingClientRect(); + + const svgWidth = containerRect.width; + const svgHeight = containerRect.height; + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect + setSvgDimensions({ width: svgWidth, height: svgHeight }); + + const startX = + rectA.left - containerRect.left + rectA.width / 2 + startXOffset; + const startY = + rectA.top - containerRect.top + rectA.height / 2 + startYOffset; + const endX = + rectB.left - containerRect.left + rectB.width / 2 + endXOffset; + const endY = + rectB.top - containerRect.top + rectB.height / 2 + endYOffset; + + const controlY = startY - curvature; + const d = `M ${startX},${startY} Q ${ + (startX + endX) / 2 + },${controlY} ${endX},${endY}`; + // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect + setPathD(d); + } + }; + + // Initialize ResizeObserver + const resizeObserver = new ResizeObserver(entries => { + // For all entries, recalculate the path + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const _entry of entries) { + updatePath(); + } + }); + + // Observe the container element + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Call the updatePath initially to set the initial path + updatePath(); + + // Clean up the observer on component unmount + return () => { + resizeObserver.disconnect(); + }; + }, [ + containerRef, + fromRef, + toRef, + curvature, + startXOffset, + startYOffset, + endXOffset, + endYOffset, + ]); + + return ( + + + + + + + + + + + + + ); +}; diff --git a/apps/docs/src/examples/auto-form.tsx b/apps/docs/src/examples/auto-form.tsx index bdccd64b2..87ff240d9 100644 --- a/apps/docs/src/examples/auto-form.tsx +++ b/apps/docs/src/examples/auto-form.tsx @@ -10,7 +10,9 @@ import { z } from 'zod'; export default function AutoFormExample() { const formSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), - email: z.string().email('Please enter a valid email address'), + email: z + .email('Please enter a valid email address') + .describe("We'll use this email to contact you. (from zod schema)"), user_type: z.enum(['admin', 'editor', 'viewer']), accept_terms: z.boolean().refine(val => val, { message: 'You must accept the terms and conditions', @@ -36,12 +38,7 @@ export default function AutoFormExample() { { id: 'email', component: props => ( - + ), }, { diff --git a/apps/docs/src/examples/button.tsx b/apps/docs/src/examples/button.tsx index 7a8571364..4cded248b 100644 --- a/apps/docs/src/examples/button.tsx +++ b/apps/docs/src/examples/button.tsx @@ -25,7 +25,7 @@ export default function ButtonExample() { Link - diff --git a/apps/docs/src/examples/input.tsx b/apps/docs/src/examples/input.tsx index 76cc38292..c0fe93052 100644 --- a/apps/docs/src/examples/input.tsx +++ b/apps/docs/src/examples/input.tsx @@ -7,7 +7,7 @@ import { z } from 'zod'; export default function InputExample() { const formSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), - email: z.string().email('Please enter a valid email address'), + email: z.email('Please enter a valid email address'), }); return ( diff --git a/apps/docs/src/vitnode.config.ts b/apps/docs/src/vitnode.config.ts index fa8a728bc..4dcda6d6d 100644 --- a/apps/docs/src/vitnode.config.ts +++ b/apps/docs/src/vitnode.config.ts @@ -8,6 +8,7 @@ export const vitNodeConfig = buildConfig({ shortTitle: 'VitNode', }, plugins: [blogPlugin()], + debug: true, i18n: { locales: [ { diff --git a/package.json b/package.json index f79929a79..836657c0d 100644 --- a/package.json +++ b/package.json @@ -18,19 +18,19 @@ "test:e2e": "turbo test:e2e" }, "devDependencies": { - "@types/node": "^24.0.12", + "@types/node": "^24.0.15", "@vitnode/eslint-config": "workspace:*", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tsx": "^4.20.3", - "turbo": "^2.5.4", + "turbo": "^2.5.5", "typescript": "^5.8.3", - "zod": "^3.25.76" + "zod": "^4.0.5" }, "engines": { "node": ">=22" }, - "packageManager": "pnpm@10.12.4", + "packageManager": "pnpm@10.13.1", "workspaces": [ "apps/*", "packages/*", diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs b/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs index 16c29ce23..0098d1c8a 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs +++ b/packages/create-vitnode-app/copy-of-vitnode-app/eslint/eslint.config.mjs @@ -1,3 +1,17 @@ import eslintVitNode from '@vitnode/eslint-config/eslint'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; -export default [...eslintVitNode]; +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default [ + ...eslintVitNode, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + }, +]; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css index 3b8486b54..58ef0849a 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/global.css @@ -47,7 +47,7 @@ --card-foreground: oklch(0.96 0.01 250); --popover: oklch(0.22 0.01 250); --popover-foreground: oklch(0.96 0.01 250); - --primary: oklch(0.51 0.16 262.61); + --primary: oklch(0.6 0.18 262.65); --primary-foreground: oklch(0.98 0 0); --secondary: oklch(0.29 0.03 264.9); --secondary-foreground: oklch(0.96 0.01 250); diff --git a/packages/create-vitnode-app/eslint.config.mjs b/packages/create-vitnode-app/eslint.config.mjs index 7f4241f66..ee68cfb76 100644 --- a/packages/create-vitnode-app/eslint.config.mjs +++ b/packages/create-vitnode-app/eslint.config.mjs @@ -1,4 +1,8 @@ import eslintVitNode from '@vitnode/eslint-config/eslint'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default [ ...eslintVitNode, @@ -10,4 +14,12 @@ export default [ { ignores: ['copy-of-vitnode-app'], }, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + }, ]; diff --git a/packages/create-vitnode-app/package.json b/packages/create-vitnode-app/package.json index 614425550..436d9d828 100644 --- a/packages/create-vitnode-app/package.json +++ b/packages/create-vitnode-app/package.json @@ -28,18 +28,18 @@ "typescript" ], "dependencies": { - "@inquirer/prompts": "^7.6.0", + "@inquirer/prompts": "^7.7.1", "commander": "^14.0.0", "ora": "^8.2.0", "picocolors": "^1.1.1", - "validate-npm-package-name": "^6.0.1" + "validate-npm-package-name": "^6.0.2" }, "devDependencies": { - "@types/node": "^24.0.12", + "@types/node": "^24.1.0", "@types/prompts": "^2.4.9", "@types/validate-npm-package-name": "^4.0.2", "@vitnode/eslint-config": "workspace:*", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "typescript": "^5.8.3" } } diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 55b084fad..493a536a1 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -69,9 +69,9 @@ export const createPackageJSON = async ({ prettier: '^3.6.2', } : {}), - turbo: '^2.5.4', + turbo: '^2.5.5', typescript: '^5.8.3', - zod: '^3.25.74', + zod: '^4.0.5', }, packageManager: `${packageManager}@${availablePackageManagers[packageManager]}`, workspaces: ['apps/*', 'plugins/*'], @@ -116,20 +116,20 @@ export const createPackageJSON = async ({ 'drizzle-kit': 'drizzle-kit', }, dependencies: { - '@hono/zod-openapi': '^0.19.8', - '@hono/zod-validator': '^0.7.0', - '@react-email/components': '^0.2.0', + '@hono/zod-openapi': '^1.0.2', + '@hono/zod-validator': '^0.7.2', + '@react-email/components': '^0.3.2', '@vitnode/core': pkgVitNodeVersion, 'drizzle-kit': '^0.31.3', - 'drizzle-orm': '^0.44.2', - hono: '^4.8.3', + 'drizzle-orm': '^0.44.3', + hono: '^4.8.5', 'next-intl': '^4.3.1', react: '^19.1', 'react-dom': '^19.1', - zod: '^3.25.67', + zod: '^4.0.5', }, devDependencies: { - '@hono/node-server': '^1.15.0', + '@hono/node-server': '^1.17.1', ...(packageManager === 'bun' ? { '@types/bun': 'latest', @@ -142,7 +142,7 @@ export const createPackageJSON = async ({ dotenv: '^17.2.0', ...(eslint ? { - eslint: '^9.30.1', + eslint: '^9.31.0', ...(mode === 'onlyApi' ? { 'prettier-plugin-tailwindcss': '^0.6.14', @@ -151,7 +151,7 @@ export const createPackageJSON = async ({ : {}), } : {}), - 'react-email': '^4.1.1', + 'react-email': '^4.2.3', 'tsc-alias': '^1.8.16', tsx: '^4.20.3', typescript: '^5.8.3', @@ -169,7 +169,7 @@ export const createPackageJSON = async ({ 'db:migrate': 'vitnode migrate', init: 'vitnode init', dev: 'vitnode init && next dev --turbopack', - build: 'next build --turbopack', + build: 'next build', start: 'next start', ...(eslint ? { @@ -185,23 +185,23 @@ export const createPackageJSON = async ({ 'drizzle-kit': 'drizzle-kit', }, dependencies: { - '@hono/zod-openapi': '^0.19.9', - '@hono/zod-validator': '^0.7.0', + '@hono/zod-openapi': '^1.0.2', + '@hono/zod-validator': '^0.7.2', '@hookform/resolvers': '^5.1.1', - '@react-email/components': '^0.2.0', + '@react-email/components': '^0.3.2', '@vitnode/core': pkgVitNodeVersion, 'babel-plugin-react-compiler': '19.1.0-rc.2', 'drizzle-kit': '^0.31.4', - 'drizzle-orm': '^0.44.2', - hono: '^4.8.4', + 'drizzle-orm': '^0.44.3', + hono: '^4.8.5', 'lucide-react': '^0.525.0', - next: '^15.3.5', + next: '^15.4.2', 'next-intl': '^4.3.4', react: '^19.1', 'react-dom': '^19.1', 'react-hook-form': '^7.60.0', sonner: '^2.0.6', - zod: '^3.25.74', + zod: '^4.0.5', }, devDependencies: { '@tailwindcss/postcss': '^4.1.11', @@ -211,13 +211,13 @@ export const createPackageJSON = async ({ '@vitnode/eslint-config': pkgVitNodeVersion, ...(eslint ? { - eslint: '^9.30.1', + eslint: '^9.31.0', 'prettier-plugin-tailwindcss': '^0.6.14', prettier: '^3.6.2', } : {}), - 'react-email': '^4.1.1', - turbo: '^2.5.4', + 'react-email': '^4.2.3', + turbo: '^2.5.5', tailwindcss: '^4.1.11', 'tw-animate-css': '^1.3.5', typescript: '^5.8.3', @@ -243,7 +243,7 @@ export const createPackageJSON = async ({ scripts: { init: 'vitnode init --web', dev: 'vitnode init --web && next dev --turbopack', - build: 'next build --turbopack', + build: 'next build', start: 'next start', ...(eslint ? { @@ -256,7 +256,7 @@ export const createPackageJSON = async ({ '@vitnode/core': pkgVitNodeVersion, 'babel-plugin-react-compiler': '19.1.0-rc.2', 'lucide-react': '^0.525.0', - next: '^15.3.5', + next: '^15.4.2', 'next-intl': '^4.3.4', react: '^19.1', 'react-dom': '^19.1', @@ -274,15 +274,14 @@ export const createPackageJSON = async ({ 'class-variance-authority': '^0.7.1', ...(eslint ? { - eslint: '^9.30.1', + eslint: '^9.31.0', } : {}), postcss: '^8.5.6', - 'react-email': '^4.1.1', tailwindcss: '^4.1.11', 'tw-animate-css': '^1.3.5', typescript: '^5.8.3', - zod: '^3.25.74', + zod: '^4.0.5', }, }; diff --git a/packages/eslint/eslint.config.mjs b/packages/eslint/eslint.config.mjs index 94643c2ce..53da1d417 100644 --- a/packages/eslint/eslint.config.mjs +++ b/packages/eslint/eslint.config.mjs @@ -8,6 +8,10 @@ import reactPlugin from 'eslint-plugin-react'; import hooksPlugin from 'eslint-plugin-react-hooks'; import reactCompiler from 'eslint-plugin-react-compiler'; import eslintReact from '@eslint-react/eslint-plugin'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default [ { @@ -69,13 +73,6 @@ export default [ }, }, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'] }, - { - languageOptions: { - parserOptions: { - project: ['./tsconfig.json'], - }, - }, - }, { rules: { '@eslint-react/no-context-provider': 'off', diff --git a/packages/eslint/package.json b/packages/eslint/package.json index f2dfa0b4e..231e27fcd 100644 --- a/packages/eslint/package.json +++ b/packages/eslint/package.json @@ -42,16 +42,16 @@ "typescript": "^5.8.3" }, "dependencies": { - "@eslint-react/eslint-plugin": "^1.52.2", - "@eslint/js": "^9.30.1", - "eslint-config-prettier": "^10.1.5", + "@eslint-react/eslint-plugin": "^1.52.3", + "@eslint/js": "^9.31.0", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-perfectionist": "^4.15.0", - "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-prettier": "^5.5.3", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-react-hooks": "6.0.0-rc1", "prettier-plugin-tailwindcss": "^0.6.14", - "typescript-eslint": "^8.36.0" + "typescript-eslint": "^8.38.0" } } diff --git a/packages/vitnode/eslint.config.mjs b/packages/vitnode/eslint.config.mjs index 16c29ce23..0098d1c8a 100644 --- a/packages/vitnode/eslint.config.mjs +++ b/packages/vitnode/eslint.config.mjs @@ -1,3 +1,17 @@ import eslintVitNode from '@vitnode/eslint-config/eslint'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; -export default [...eslintVitNode]; +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default [ + ...eslintVitNode, + { + languageOptions: { + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + }, +]; diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 1d99497fa..af2dff761 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -19,7 +19,7 @@ "core" ], "peerDependencies": { - "@hono/zod-openapi": "0.19.x", + "@hono/zod-openapi": "1.0.x", "@swc/cli": "0.6.x", "@swc/core": "1.12.x", "@types/react": "19.1.x", @@ -33,38 +33,38 @@ "react-dom": "19.1.x", "react-hook-form": "^7.x.x", "typescript": "^5.8.x", - "zod": "3.x.x" + "zod": "4.x.x" }, "devDependencies": { - "@hono/zod-openapi": "^0.19.9", - "@hono/zod-validator": "^0.7.0", + "@hono/zod-openapi": "^1.0.2", + "@hono/zod-validator": "^0.7.2", "@hookform/resolvers": "^5.1.1", - "@react-email/components": "^0.2.0", + "@react-email/components": "^0.3.2", "@swc/cli": "0.6.0", - "@swc/core": "^1.12.11", + "@swc/core": "^1.13.1", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", - "@types/node": "^24.0.12", + "@types/node": "^24.1.0", "@types/nodemailer": "^6.4.17", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", + "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^3.2.4", "@vitnode/eslint-config": "workspace:*", "chokidar": "^4.0.3", "concurrently": "^9.2.0", "dotenv": "^17.2.0", "drizzle-kit": "^0.31.4", - "drizzle-orm": "^0.44.2", - "eslint": "^9.30.1", - "hono": "^4.8.4", + "drizzle-orm": "^0.44.3", + "eslint": "^9.31.0", + "hono": "^4.8.5", "jsdom": "^26.1.0", "lucide-react": "^0.525.0", - "next": "^15.3.5", + "next": "^15.4.2", "next-intl": "^4.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-email": "^4.1.1", + "react-email": "^4.2.3", "react-hook-form": "^7.60.0", "sonner": "^2.0.6", "tailwindcss": "^4.1.11", @@ -75,7 +75,7 @@ "typescript": "^5.8.3", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", - "zod": "^3.25.76" + "zod": "^4.0.5" }, "bin": { "vitnode": "./dist/scripts/scripts.js" @@ -110,7 +110,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@hono/swagger-ui": "^0.5.2", - "@tanstack/react-query": "^5.82.0", + "@tanstack/react-query": "^5.83.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -121,7 +121,7 @@ "radix-ui": "^1.4.2", "rate-limiter-flexible": "^7.1.1", "react-scan": "^0.4.3", - "resend": "^4.6.0", + "resend": "^4.7.0", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.5", "vaul": "^1.1.2" diff --git a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts index e0c2a61bc..8fad0b330 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-in.route.ts @@ -7,7 +7,7 @@ import { UserModel } from '@/api/models/user'; import { CONFIG_PLUGIN } from '@/config'; export const zodSignInSchema = z.object({ - email: z.string().email().toLowerCase().openapi({ + email: z.email().toLowerCase().openapi({ example: 'test@test.com', }), password: z.string().openapi({ diff --git a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts index 2fc877a46..2d8b6a3a0 100644 --- a/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/sign-up.route.ts @@ -10,7 +10,7 @@ import { SessionModel } from '../../../models/session'; const nameRegex = /^(?!.* {2})[\p{L}\p{N}._@ -]*$/u; export const zodSignUpSchema = z.object({ - email: z.string().email().toLowerCase().openapi({ + email: z.email().toLowerCase().openapi({ example: 'test@test.com', }), name: z @@ -50,7 +50,7 @@ export const signUpRoute = buildRoute({ schema: z.object({ id: z.number(), emailVerified: z.boolean(), - email: z.string().email(), + email: z.email(), }), }, }, diff --git a/packages/vitnode/src/components/form/auto-form.tsx b/packages/vitnode/src/components/form/auto-form.tsx index 193f4bd50..963e4c677 100644 --- a/packages/vitnode/src/components/form/auto-form.tsx +++ b/packages/vitnode/src/components/form/auto-form.tsx @@ -1,50 +1,86 @@ 'use client'; -import type { DefaultValues, Mode, UseFormReturn } from 'react-hook-form'; -import type { z } from 'zod'; - import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslations } from 'next-intl'; -import { useForm } from 'react-hook-form'; - -import { getDefaultValues, getObjectFormSchema } from '@/lib/helpers/auto-form'; +import { + type ControllerRenderProps, + type FieldPath, + type FieldValues, + type Mode, + useForm, + type UseFormReturn, +} from 'react-hook-form'; +import * as z from 'zod'; import type { routeMiddlewareSchema } from '../../api/modules/middleware/route'; -import type { ItemAutoFormProps } from './fields/item'; import { useCaptcha } from '../../hooks/use-captcha'; +import { + getDefaults, + getNestedParam, + getZodInputParams, +} from '../../lib/helpers/auto-form'; import { Button } from '../ui/button'; import { DialogClose, DialogFooter, useDialog } from '../ui/dialog'; -import { Form } from '../ui/form'; -import { ItemAutoForm } from './fields/item'; +import { Form, FormField } from '../ui/form'; + +export interface ItemAutoFormComponentProps { + description?: React.ReactNode; + field: ControllerRenderProps; + label?: React.ReactNode; + otherProps: { + enum?: string[]; + isOptional?: boolean; + maxLength?: number; + minLength?: number; + pattern?: string; + type?: string; + }; +} -export type AutoFormOnSubmit = ( +type ItemAutoFormProps< + T extends z.ZodObject = z.ZodObject, + TName extends FieldPath> = FieldPath>, +> = + | { + component: (props: ItemAutoFormComponentProps) => React.ReactNode; + id: TName; + } + | { + component?: never; + description?: React.ReactNode; + id: TName; + label?: React.ReactNode; + }; + +export type AutoFormOnSubmit< + T extends z.ZodObject, + TContext = unknown, +> = ( values: z.infer, - form: UseFormReturn>, + form: UseFormReturn, TContext, z.output>, options: { captchaToken: string; }, ) => Promise | void; export function AutoForm< - T extends - | z.ZodEffects> - | z.ZodObject, + T extends z.ZodObject, TContext = unknown, >({ formSchema, + mode, onSubmit: onSubmitProp, + captcha, fields, submitButtonProps, - mode, - captcha, ...props }: Omit, 'onSubmit'> & { captcha?: z.infer['captcha']; fields: ItemAutoFormProps[]; formSchema: T; mode?: Mode; - onSubmit?: AutoFormOnSubmit; + onSubmit?: AutoFormOnSubmit; submitButtonProps?: Omit< React.ComponentProps, 'isLoading' | 'type' @@ -56,21 +92,19 @@ export function AutoForm< onReset: onResetCaptcha, } = useCaptcha(captcha); const { setIsDirty } = useDialog(); - const objectFormSchema = getObjectFormSchema(formSchema); - const defaultValues = getDefaultValues(objectFormSchema) as DefaultValues< - z.infer - >; const t = useTranslations('core.global'); - const form = useForm, TContext>({ + const jsonSchema: z.core.JSONSchema.JSONSchema = z.toJSONSchema(formSchema); + const inputParams = getZodInputParams(jsonSchema); + const form = useForm, TContext, z.core.output>({ resolver: zodResolver(formSchema), - defaultValues, + defaultValues: getDefaults(jsonSchema), mode, }); const onSubmit = async (values: z.infer) => { const parsedValues = formSchema.safeParse(values); if (parsedValues.success) { - await onSubmitProp?.(parsedValues.data as z.infer, form, { + await onSubmitProp?.(parsedValues.data, form, { captchaToken: captcha ? await getTokenCaptcha() : '', }); @@ -98,9 +132,69 @@ export function AutoForm< return (
- {fields.map(field => ( - - ))} + {fields.map(item => { + const params = getNestedParam(inputParams, item.id); + if (!params) return null; + + if (!item.component && (item.label || item.description)) { + return ( +
+ {item.label && ( + + {item.label} + + )} + {item.description && ( +
+ {item.description} +
+ )} +
+ ); + } + + if (!item.component) return null; + + return ( + { + return ( + <> + {item.component({ + field, + description: + typeof params.description === 'string' + ? params.description + : '', + otherProps: { + isOptional: !params.required, + enum: Array.isArray(params.enum) + ? params.enum + : undefined, + maxLength: + typeof params.maxLength === 'number' + ? params.maxLength + : undefined, + minLength: + typeof params.minLength === 'number' + ? params.minLength + : undefined, + pattern: + typeof params.pattern === 'string' + ? params.pattern + : undefined, + type: params.type === 'string' ? params.type : undefined, + }, + })} + + ); + }} + /> + ); + })} + {captcha &&
} {setIsDirty ? ( diff --git a/packages/vitnode/src/components/form/fields/checkbox.tsx b/packages/vitnode/src/components/form/fields/checkbox.tsx index 02dcf75fc..70f323d8b 100644 --- a/packages/vitnode/src/components/form/fields/checkbox.tsx +++ b/packages/vitnode/src/components/form/fields/checkbox.tsx @@ -1,45 +1,41 @@ -import type { z } from 'zod'; - -import { Checkbox } from '@/components/ui/checkbox'; -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; - -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; +import { Checkbox } from '../../ui/checkbox'; +import { FormControl, FormItem, FormMessage } from '../../ui/form'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormCheckbox({ +export const AutoFormCheckbox = ({ label, - field, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shape: _, description, + otherProps: { isOptional }, + field, ...props -}: ItemAutoFormComponentProps & - Omit, 'checked'> & { - description?: React.ReactNode; - label?: React.ReactNode; - }) { +}: ItemAutoFormComponentProps & + Omit, 'checked'>) => { return ( { field.onChange(e); props.onCheckedChange?.(e); }} + {...field} {...props} /> {!!(label ?? description) && (
- {label && {label}} + {label && ( + {label} + )} {description && {description}}
)}
); -} +}; diff --git a/packages/vitnode/src/components/form/fields/combobox-async.tsx b/packages/vitnode/src/components/form/fields/combobox-async.tsx index aafca888e..7bf66e800 100644 --- a/packages/vitnode/src/components/form/fields/combobox-async.tsx +++ b/packages/vitnode/src/components/form/fields/combobox-async.tsx @@ -1,5 +1,3 @@ -import type { z } from 'zod'; - import { useQuery } from '@tanstack/react-query'; import { Check, ChevronsUpDown } from 'lucide-react'; import { useTranslations } from 'next-intl'; @@ -23,27 +21,25 @@ import { } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; import { Skeleton } from '../../ui/skeleton'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormComboboxAsync({ +export const AutoFormComboboxAsync = ({ label, field, description, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shape: _s, placeholder, className, id, + otherProps: { isOptional }, searchPlaceholder, fetchData, ...props -}: ItemAutoFormComponentProps & +}: ItemAutoFormComponentProps & Omit, 'role' | 'variant'> & { - description?: React.ReactNode; fetchData: (params: { search: string }) => | Promise< { @@ -56,10 +52,9 @@ export function AutoFormComboboxAsync({ value: string; }[]; id: string; - label?: React.ReactNode; placeholder?: string; searchPlaceholder?: string; - }) { + }) => { const t = useTranslations('core.global'); const [search, setSearch] = React.useState(''); const { data, isLoading } = useQuery({ @@ -75,7 +70,7 @@ export function AutoFormComboboxAsync({ return ( - {label && {label}} + {label && {label}} @@ -91,9 +86,7 @@ export function AutoFormComboboxAsync({ variant="outline" {...props} > - {field.value && field.value.label - ? field.value.label - : (placeholder ?? t('select_option'))} + {field.value?.label ?? placeholder ?? t('select_option')} @@ -155,4 +148,4 @@ export function AutoFormComboboxAsync({ ); -} +}; diff --git a/packages/vitnode/src/components/form/fields/combobox.tsx b/packages/vitnode/src/components/form/fields/combobox.tsx index 3b9064784..d4ebfce73 100644 --- a/packages/vitnode/src/components/form/fields/combobox.tsx +++ b/packages/vitnode/src/components/form/fields/combobox.tsx @@ -1,5 +1,3 @@ -import type { z } from 'zod'; - import { Check, ChevronsUpDown } from 'lucide-react'; import { useTranslations } from 'next-intl'; import React from 'react'; @@ -19,37 +17,32 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; -import { getBaseSchema } from '@/lib/helpers/auto-form'; import { cn } from '@/lib/utils'; -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormCombobox({ +export const AutoFormCombobox = ({ label, field, description, - shape, placeholder, className, + otherProps: { enum: enumValues = [], isOptional }, labels = [], searchPlaceholder, ...props -}: ItemAutoFormComponentProps & +}: ItemAutoFormComponentProps & Omit, 'role' | 'variant'> & { - description?: React.ReactNode; - label?: React.ReactNode; labels?: { label: string; value: string }[]; placeholder?: string; searchPlaceholder?: string; - }) { + }) => { const t = useTranslations('core.global'); - const baseValues = ( - getBaseSchema(shape, true) as unknown as z.ZodEnum<[string, ...string[]]> - )._def.values; - const values: { label: string; value: string }[] = baseValues.map(value => { + + const values: { label: string; value: string }[] = enumValues.map(value => { const label = labels.find(l => l.value === value)?.label; return { @@ -60,7 +53,7 @@ export function AutoFormCombobox({ return ( - {label && {label}} + {label && {label}} @@ -119,4 +112,4 @@ export function AutoFormCombobox({ ); -} +}; diff --git a/packages/vitnode/src/components/form/fields/input.tsx b/packages/vitnode/src/components/form/fields/input.tsx index 255b50c0e..7ad2b3f85 100644 --- a/packages/vitnode/src/components/form/fields/input.tsx +++ b/packages/vitnode/src/components/form/fields/input.tsx @@ -1,33 +1,26 @@ -import type { z } from 'zod'; - -import React from 'react'; - -import { FormControl, FormItem, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; - -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; +import { FormControl, FormItem, FormMessage } from '../../ui/form'; +import { Input } from '../../ui/input'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormInput({ +export const AutoFormInput = ({ label, - field, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - shape: _, description, + otherProps: { isOptional, maxLength, minLength, pattern, type }, + field, ...props -}: ItemAutoFormComponentProps & - Omit, 'value'> & { - description?: React.ReactNode; - label?: React.ReactNode; - }) { +}: ItemAutoFormComponentProps & + Omit, 'value'>) => { return ( - {label && {label}} - + {label && {label}} { field.onBlur(); props.onBlur?.(e); @@ -36,6 +29,8 @@ export function AutoFormInput({ field.onChange(e); props.onChange?.(e); }} + pattern={pattern} + type={type ?? 'text'} value={field.value ?? ''} {...props} /> @@ -45,4 +40,4 @@ export function AutoFormInput({ ); -} +}; diff --git a/packages/vitnode/src/components/form/fields/item.tsx b/packages/vitnode/src/components/form/fields/item.tsx deleted file mode 100644 index 5b70efc22..000000000 --- a/packages/vitnode/src/components/form/fields/item.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { - ControllerRenderProps, - FieldPath, - FieldValues, -} from 'react-hook-form'; -import type { z } from 'zod'; - -import { FormField } from '@/components/ui/form'; -import { getShapeFromSchema } from '@/lib/helpers/auto-form'; - -export interface ItemAutoFormComponentProps< - T extends z.ZodTypeAny, - TName extends FieldPath> = FieldPath>, -> { - field: ControllerRenderProps; - shape: z.ZodAny; -} - -export interface ItemAutoFormProps< - T extends z.ZodTypeAny, - TName extends FieldPath> = FieldPath>, -> { - component: (props: ItemAutoFormComponentProps) => React.ReactNode; - id: TName; -} - -export function ItemAutoForm< - T extends - | z.ZodEffects> - | z.ZodObject, - TName extends FieldPath> = FieldPath>, ->({ - id, - component, - formSchema, -}: ItemAutoFormProps & { formSchema: T }) { - let shape: null | z.ZodAny = null; - const ids = id.split('.'); - for (const id of ids) { - shape = getShapeFromSchema( - shape ? (shape as unknown as z.ZodObject) : formSchema, - id, - ); - } - if (!shape) return null; - - return ( - { - return <>{component({ field, shape })}; - }} - /> - ); -} diff --git a/packages/vitnode/src/components/form/fields/radio-group.tsx b/packages/vitnode/src/components/form/fields/radio-group.tsx index dc2f78a52..91005e3eb 100644 --- a/packages/vitnode/src/components/form/fields/radio-group.tsx +++ b/packages/vitnode/src/components/form/fields/radio-group.tsx @@ -1,5 +1,3 @@ -import type { z } from 'zod'; - import React from 'react'; import { @@ -9,30 +7,24 @@ import { FormMessage, } from '@/components/ui/form'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; -import { getBaseSchema } from '@/lib/helpers/auto-form'; -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormRadioGroup({ +export const AutoFormRadioGroup = ({ label, field, description, - shape, + otherProps: { enum: enumValues = [], isOptional }, labels = [], ...props -}: ItemAutoFormComponentProps & +}: ItemAutoFormComponentProps & Omit, 'value'> & { - description?: React.ReactNode; - label?: React.ReactNode; labels?: { label: string; value: string }[]; - }) { - const baseValues = ( - getBaseSchema(shape, true) as unknown as z.ZodEnum<[string, ...string[]]> - )._def.values; - const values: { label: string; value: string }[] = baseValues.map(value => { + }) => { + const values: { label: string; value: string }[] = enumValues.map(value => { const label = labels.find(l => l.value === value)?.label; return { @@ -43,7 +35,7 @@ export function AutoFormRadioGroup({ return ( - {label && {label}} + {label && {label}} ({ ); -} +}; diff --git a/packages/vitnode/src/components/form/fields/select.tsx b/packages/vitnode/src/components/form/fields/select.tsx index 2d75fd168..87960c915 100644 --- a/packages/vitnode/src/components/form/fields/select.tsx +++ b/packages/vitnode/src/components/form/fields/select.tsx @@ -1,5 +1,3 @@ -import type { z } from 'zod'; - import { useTranslations } from 'next-intl'; import React from 'react'; @@ -11,33 +9,27 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { getBaseSchema } from '@/lib/helpers/auto-form'; -import type { ItemAutoFormComponentProps } from './item'; +import type { ItemAutoFormComponentProps } from '../auto-form'; import { AutoFormDesc } from '../common/desc'; import { AutoFormLabel } from '../common/label'; -export function AutoFormSelect({ +export const AutoFormSelect = ({ label, field, description, - shape, + otherProps: { enum: enumValues = [], isOptional }, placeholder, labels = [], ...props -}: ItemAutoFormComponentProps & +}: ItemAutoFormComponentProps & Omit, 'value'> & { - description?: React.ReactNode; - label?: React.ReactNode; labels?: { label: string; value: string }[]; placeholder?: string; - }) { + }) => { const t = useTranslations('core.global'); - const baseValues = ( - getBaseSchema(shape, true) as unknown as z.ZodEnum<[string, ...string[]]> - )._def.values; - const values: { label: string; value: string }[] = baseValues.map(value => { + const values: { label: string; value: string }[] = enumValues.map(value => { const label = labels.find(l => l.value === value)?.label; return { @@ -52,7 +44,7 @@ export function AutoFormSelect({ return ( - {label && {label}} + {label && {label}}