├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── components.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── prisma └── schema.prisma ├── public └── favicon.ico ├── src ├── components │ ├── layout │ │ ├── AuthRoute.tsx │ │ ├── GuestRoute.tsx │ │ ├── HeadMetaData.tsx │ │ ├── Header.tsx │ │ ├── PageContainer.tsx │ │ └── SectionContainer.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── sonner.tsx │ │ └── textarea.tsx ├── env.js ├── features │ ├── auth │ │ ├── components │ │ │ └── RegisterFormInner.tsx │ │ ├── forms │ │ │ └── register.ts │ │ └── pages │ │ │ ├── LoginPage.tsx │ │ │ └── RegisterPage.tsx │ └── profile │ │ ├── components │ │ └── EditProfileFormInner.tsx │ │ ├── forms │ │ └── edit-profile.ts │ │ └── pages │ │ └── ProfilePage.tsx ├── lib │ ├── supabase │ │ ├── authErrorCodes.ts │ │ ├── bucket.ts │ │ ├── client.ts │ │ └── server.ts │ └── utils.ts ├── pages │ ├── _app.tsx │ ├── api │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ ├── login.tsx │ ├── profile.tsx │ └── register.tsx ├── schemas │ └── auth.ts ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ ├── auth.ts │ │ │ └── profile.ts │ │ └── trpc.ts │ └── db.ts ├── styles │ └── globals.css └── utils │ └── api.ts ├── start-database.sh ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Connect to Supabase via connection pooling with Supavisor. 3 | DATABASE_URL="postgresql://postgres.pzdidkaxnpkfrnpvfaeb:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true" 4 | 5 | # Direct connection to the database. Used for migrations. 6 | DIRECT_URL="postgresql://postgres.pzdidkaxnpkfrnpvfaeb:[YOUR-PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres" 7 | 8 | NEXT_PUBLIC_BASE_URL="http://localhost:3000" 9 | 10 | # Supabase 11 | NEXT_PUBLIC_SUPABASE_URL="" 12 | NEXT_PUBLIC_SUPABASE_ANON_KEY="" 13 | SUPABASE_SERVICE_ROLE_KEY="" -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "project": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "extends": [ 11 | "next/core-web-vitals", 12 | "plugin:@typescript-eslint/recommended-type-checked", 13 | "plugin:@typescript-eslint/stylistic-type-checked" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/array-type": "off", 17 | "@typescript-eslint/consistent-type-definitions": "off", 18 | "@typescript-eslint/consistent-type-imports": [ 19 | "warn", 20 | { 21 | "prefer": "type-imports", 22 | "fixStyle": "inline-type-imports" 23 | } 24 | ], 25 | "@typescript-eslint/no-unused-vars": [ 26 | "warn", 27 | { 28 | "argsIgnorePattern": "^_" 29 | } 30 | ], 31 | "@typescript-eslint/require-await": "off", 32 | "@typescript-eslint/no-misused-promises": [ 33 | "error", 34 | { 35 | "checksVoidReturn": { 36 | "attributes": false 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | module.exports = config; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | db.sqlite 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | 45 | # idea files 46 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qepo Lu Q\*nt\*l -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you are using `appDir` then you must comment the below `i18n` config out. 13 | * 14 | * @see https://github.com/vercel/next.js/issues/41980 15 | */ 16 | i18n: { 17 | locales: ["en"], 18 | defaultLocale: "en", 19 | }, 20 | transpilePackages: ["geist"], 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qepo-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build", 8 | "check": "next lint && tsc --noEmit", 9 | "db:generate": "prisma migrate dev", 10 | "db:migrate": "prisma migrate deploy", 11 | "db:push": "prisma db push", 12 | "db:studio": "prisma studio", 13 | "dev": "next dev --turbo", 14 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 15 | "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 16 | "postinstall": "prisma generate", 17 | "lint": "next lint", 18 | "lint:fix": "next lint --fix", 19 | "preview": "next build && next start", 20 | "start": "next start", 21 | "typecheck": "tsc --noEmit" 22 | }, 23 | "dependencies": { 24 | "@hookform/resolvers": "^3.10.0", 25 | "@prisma/client": "^5.14.0", 26 | "@radix-ui/react-avatar": "^1.1.2", 27 | "@radix-ui/react-checkbox": "^1.1.3", 28 | "@radix-ui/react-label": "^2.1.1", 29 | "@radix-ui/react-slot": "^1.1.1", 30 | "@supabase/ssr": "^0.5.2", 31 | "@supabase/supabase-js": "^2.48.1", 32 | "@t3-oss/env-nextjs": "^0.10.1", 33 | "@tanstack/react-query": "^5.50.0", 34 | "@trpc/client": "^11.0.0-rc.446", 35 | "@trpc/next": "^11.0.0-rc.446", 36 | "@trpc/react-query": "^11.0.0-rc.446", 37 | "@trpc/server": "^11.0.0-rc.446", 38 | "class-variance-authority": "^0.7.1", 39 | "clsx": "^2.1.1", 40 | "geist": "^1.3.0", 41 | "lucide-react": "^0.473.0", 42 | "next": "^15.0.1", 43 | "next-themes": "^0.4.4", 44 | "react": "^18.3.1", 45 | "react-dom": "^18.3.1", 46 | "react-hook-form": "^7.54.2", 47 | "react-icons": "^5.4.0", 48 | "sonner": "^1.7.2", 49 | "superjson": "^2.2.1", 50 | "tailwind-merge": "^2.6.0", 51 | "tailwindcss-animate": "^1.0.7", 52 | "unique-username-generator": "^1.4.0", 53 | "zod": "^3.24.1" 54 | }, 55 | "devDependencies": { 56 | "@types/eslint": "^8.56.10", 57 | "@types/node": "^20.14.10", 58 | "@types/react": "^18.3.3", 59 | "@types/react-dom": "^18.3.0", 60 | "@typescript-eslint/eslint-plugin": "^8.1.0", 61 | "@typescript-eslint/parser": "^8.1.0", 62 | "eslint": "^8.57.0", 63 | "eslint-config-next": "^15.0.1", 64 | "postcss": "^8.4.39", 65 | "prettier": "^3.3.2", 66 | "prettier-plugin-tailwindcss": "^0.6.5", 67 | "prisma": "^5.14.0", 68 | "tailwindcss": "^3.4.3", 69 | "typescript": "^5.5.3" 70 | }, 71 | "ct3aMetadata": { 72 | "initVersion": "7.38.1" 73 | }, 74 | "packageManager": "npm@10.2.3" 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | directUrl = env("DIRECT_URL") 12 | } 13 | 14 | model Post { 15 | id Int @id @default(autoincrement()) 16 | name String 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | 20 | @@index([name]) 21 | } 22 | 23 | model Profile { 24 | userId String @id 25 | 26 | email String @unique 27 | username String @unique 28 | 29 | bio String? 30 | profilePictureUrl String? 31 | } 32 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodevoid/qepo-nextjs/fafc735dd857320b4ff2cb25894086f713370338/public/favicon.ico -------------------------------------------------------------------------------- /src/components/layout/AuthRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { type PropsWithChildren, useEffect } from "react"; 3 | import { supabase } from "~/lib/supabase/client"; 4 | 5 | export const AuthRoute = (props: PropsWithChildren) => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | void (async function () { 10 | const { data } = await supabase.auth.getUser(); 11 | 12 | if (!data.user) { 13 | await router.replace("/"); 14 | } 15 | })(); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | return props.children; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/layout/GuestRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { type PropsWithChildren, useEffect } from "react"; 3 | import { supabase } from "~/lib/supabase/client"; 4 | 5 | export const GuestRoute = (props: PropsWithChildren) => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | void (async function () { 10 | const { data } = await supabase.auth.getUser(); 11 | 12 | if (data.user) { 13 | await router.replace("/"); 14 | } 15 | })(); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | return props.children; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/layout/HeadMetaData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { env } from "~/env"; 4 | 5 | export const HeadMetaData: React.FC<{ 6 | title?: string; 7 | metaDescription?: string; 8 | // ogImageUrl?: string; 9 | pathname?: string; 10 | }> = ({ 11 | title = "Konten kamu berharga", 12 | metaDescription, 13 | // ogImageUrl = env.NEXT_PUBLIC_OG_IMAGE_URL, 14 | pathname = "", 15 | }) => { 16 | const defaultTitle = "Qepo Lu"; 17 | 18 | const baseUrl = 19 | process.env.NODE_ENV === "development" 20 | ? "http://localhost:3000" 21 | : env.NEXT_PUBLIC_BASE_URL; 22 | 23 | const pageUrl = new URL(pathname, baseUrl).toString(); 24 | 25 | return ( 26 | 27 | {title + " | " + defaultTitle} 28 | 29 | 30 | {/* metadata */} 31 | 32 | 33 | {/* */} 34 | 35 | 36 | 37 | {/* */} 38 | 39 | 40 | 41 | 42 | 43 | 44 | {/* */} 45 | 46 | 47 | ); 48 | }; -------------------------------------------------------------------------------- /src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const Header = () => { 4 | return ( 5 |
6 | 10 | Qepo 11 | 12 |
13 | ); 14 | }; -------------------------------------------------------------------------------- /src/components/layout/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import { cn } from "~/lib/utils"; 3 | import { HeadMetaData } from "./HeadMetaData"; 4 | import { Header } from "./Header"; 5 | 6 | type PageContainerProps = { 7 | withHeader?: boolean; 8 | withFooter?: boolean; 9 | }; 10 | 11 | export const PageContainer = forwardRef< 12 | HTMLElement, 13 | React.HTMLAttributes & PageContainerProps 14 | >( 15 | ( 16 | { className, children, withHeader = true, withFooter = true, ...props }, 17 | ref, 18 | ) => { 19 | return ( 20 |
21 | 22 | {withHeader &&
} 23 |
24 | {children} 25 |
26 | {withFooter && ( 27 |
28 |

29 | © 2024 Fanattic. All rights reserved 30 |

31 |
32 | )} 33 |
34 | ); 35 | }, 36 | ); 37 | 38 | PageContainer.displayName = "PageContainer"; -------------------------------------------------------------------------------- /src/components/layout/SectionContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import { cn } from "~/lib/utils"; 3 | 4 | type SectionContainerProps = { 5 | padded?: boolean; 6 | containerClassName?: string; 7 | minFullscreen?: boolean; 8 | }; 9 | 10 | export const SectionContainer = forwardRef< 11 | HTMLElement, 12 | React.HTMLAttributes & SectionContainerProps 13 | >(({ className, children, padded, containerClassName, ...props }, ref) => { 14 | return ( 15 |
16 |
27 | {children} 28 |
29 |
30 | ); 31 | }); 32 | 33 | SectionContainer.displayName = "SectionContainer"; 34 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ThemeProvider as NextThemesProvider } from "next-themes" 3 | 4 | export function ThemeProvider({ 5 | children, 6 | ...props 7 | }: React.ComponentProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "~/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "~/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { Check } from "lucide-react" 4 | 5 | import { cn } from "~/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "~/lib/utils" 14 | import { Label } from "~/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |