diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..db5ddae --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +.next +node_modules \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..8bcd21c --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,76 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fs = require("fs") + +module.exports = { + extends: [ + "next", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:tailwindcss/recommended", + ], + parserOptions: { + babelOptions: { + presets: [require.resolve("next/babel")], + }, + }, + rules: { + "testing-library/prefer-screen-queries": "off", + "@next/next/no-html-link-for-pages": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "sort-imports": [ + "error", + { + ignoreCase: true, + ignoreDeclarationSort: true, + }, + ], + "tailwindcss/classnames-order": "off", + "import/order": [ + 1, + { + groups: ["external", "builtin", "internal", "sibling", "parent", "index"], + pathGroups: [ + ...getDirectoriesToSort().map((singleDir) => ({ + pattern: `${singleDir}/**`, + group: "internal", + })), + { + pattern: "env", + group: "internal", + }, + { + pattern: "theme", + group: "internal", + }, + { + pattern: "public/**", + group: "internal", + position: "after", + }, + ], + pathGroupsExcludedImportTypes: ["internal"], + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + }, +} + +function getDirectoriesToSort() { + const ignoredSortingDirectories = [".git", ".next", ".vscode", "node_modules"] + return getDirectories(process.cwd()).filter((f) => !ignoredSortingDirectories.includes(f)) +} + +function getDirectories(path) { + return fs.readdirSync(path).filter(function (file) { + return fs.statSync(path + "/" + file).isDirectory() + }) +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b7425b9 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts=true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..db5ddae --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +.next +node_modules \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8905a54 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,119 @@ +# Code of Conduct + +## Conventional Commits + +Developers should use the Conventional Commits standard when committing changes to the codebase. + +| Type | Description | +| -------- | --------------------------------------------------------------------- | +| feat | Declares a new feature has been added | +| fix | Declares a bug have been fixed | +| chore | Declares changes which don’t modify source or test files (eg. assets) | +| ci | Declares a change on the CI or CD process | +| build | Declares changes on the build setup | +| docs | Declares changes on documentation | +| style | Declares changes on code style | +| refactor | Declares a change of code without an effective change on the program | +| perf | Declares a change on performance | +| revert | Declares that a previous commit has been reverted | +| test | Declares changes on tests | + +### Examples + +#### Commit Message + +``` +refactor: adjust vehicle texture size [#ISSUENUMBER] +refactor: adjust vehicle texture size [NOISSUE] +``` + +#### Branch Name + +``` +refactor/#ISSUENUMBER_adjust-vehicle-texture-size +refactor/NOISSUE_adjust-vehicle-texture-size +``` + +## Contributing + +Developers should follow the following guidelines when contributing to the project: + +### 1. Create a new branch + +When starting work on a new feature or bug fix, create a new branch from the `main` branch. The name +of the branch should be descriptive and should include the issue number and a short description of +the feature or bug fix. For example, if you are working on issue #123, the branch name should +be `feat/#123_add-new-feature`. + +### 2. Commit changes + +When committing changes to the codebase, developers should follow +the [Conventional Commits](#conventional-commits) standard. This will ensure that the commit +messages are consistent and descriptive, and will allow the commit history to be automatically +parsed to generate release notes. + +### 3. Create a draft pull request + +After committing changes to the codebase, create a draft pull request to inform other developers +that you are working on a new feature or bug fix. The pull request should be kept in draft mode +until the feature or bug fix is complete. + +### 4. Create a pull request + +When the feature or bug fix is complete, mark the pull request as ready to review to merge the +changes into the `main` branch. The pull request should be reviewed by at least one other developer +before it can be merged. + +### 5. Review pull request + +When a pull request is marked as ready for review, it should be reviewed by at least one other +developer. The reviewer should verify that the code meets +the [Definition of Done](#definition-of-done). + +### 6. Merge pull request + +Once the pull request has been reviewed and approved, it can be merged into the `main` branch. The +pull request should be merged using the "Rebase and merge" option to ensure that the commit history +remains clean and concise. + +## Definition of Done + +### 1. Code meets coding standards + +All code must adhere to the rules defined in the Clean Code handbook for at least level L1. Level L2 +rules should also be taken into consideration. Specifically, emphasis should be placed on: + +1. Correct abstraction level: The code should have a clear and appropriate level of abstraction, + with well-defined interfaces and separation of concerns. +2. Class diagram: The class diagram should be clear and well-organized, with high cohesion and low + coupling between classes. +3. Correct error handling: The code should handle errors correctly, including validating arguments + and handling exceptions in a consistent and appropriate manner. + +### 2. Unit tests pass + +All code changes must be accompanied by unit tests that verify the expected behavior of the code. +These tests must pass without any errors or failures before the code can be considered complete. + +### 3. Code is reviewed + +All code must be reviewed by at least one other developer to ensure quality and compliance with +coding standards. The code review should focus on identifying any bugs, security vulnerabilities, or +design flaws that could impact the quality or maintainability of the code. + +### 4. Documentation is complete + +All code must be fully documented, including comments within the code and external documentation +such as user manuals. The documentation should be comprehensive and accurate, and should provide +enough detail for other developers and stakeholders to understand the code. + +### 5. Acceptance criteria are met + +The code must meet all of the acceptance criteria as defined by the stakeholders. These acceptance +criteria are used as a basis for verifying that the code meets the intended requirements + +### 6. Security is considered + +The code must be reviewed for security vulnerabilities and any identified issues must be addressed. +The code should be designed with security in mind, and should be subject to regular security testing +to identify any new vulnerabilities. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6aab65b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1 @@ +Copyright © 2023 Boostvolt (Jan). All rights reserved. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec6370e --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# iCover + +[![iCover Banner](public/assets/brand/banner.png)](https://icover.vercel.app/) + +Design a captivating and visually appealing cover artwork for your Apple Music playlist to engage listeners and convey the theme or mood of the curated songs. [icover.vercel.app](https://icover.vercel.app) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d7a4a65 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Reporting Security Issues + +If you believe you have found a security vulnerability in the codebase, we encourage you to let us +know right away. + +We will investigate all legitimate reports and do our best to quickly fix the problem. \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..18d5e68 Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..1071e11 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from "next" +import { ThemeProvider } from "@/components/theme-provider" +import "@/styles/globals.css" + +export const metadata: Metadata = { + openGraph: { + title: "iCover", + description: + "Design a captivating and visually appealing cover artwork for your Apple Music playlist to engage listeners and convey the theme or mood of the curated songs.", + url: "https://icover.vercel.app", + siteName: "iCover", + images: [ + { + url: "/assets/brand/banner.png", + width: 1280, + height: 640, + alt: "iCover Banner", + }, + ], + locale: "en_US", + type: "website", + }, +} + +interface RootLayoutProps { + children: React.ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + + + + + {children} + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..003b40e --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,300 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import html2canvas from "html2canvas" +import { Cog, Info } from "lucide-react" +import Image from "next/image" +import { useForm } from "react-hook-form" +import * as z from "zod" +import AppleMusic from "@/components/icons/apple-music" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Avatar, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Separator } from "@/components/ui/separator" +import { Switch } from "@/components/ui/switch" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { SfProDisplay } from "@/lib/fonts/sf-pro-display" + +const formSchema = z.object({ + imageFormat: z.enum(["png", "jpeg"]), + appleMusicLogo: z.boolean(), + bigTitle: z.string(), + subTitle: z.string(), + footer: z.string(), + gradient: z.string(), +}) + +export default function Home() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: formSchema.parse({ + imageFormat: "png", + appleMusicLogo: true, + bigTitle: "", + subTitle: "", + footer: "", + gradient: "0", + }), + }) + + const handleDownload = () => { + const element = document.getElementById("coverElement") + if (element) { + html2canvas(element).then((canvas) => { + const link = document.createElement("a") + link.download = `playlist-cover.${form.watch("imageFormat")}` + link.href = canvas.toDataURL() + link.click() + }) + } + } + + return ( +
+ + +
+ +
+
+ iCover +
+ +
+ + + + + + + Image Format + ( + + + + PNG + JPEG + + + + )} + /> + + +
+
+
+ +
+
+ {/* Background Image */} + 0 + {/* Apple Logo */} + {form.watch("appleMusicLogo") && ( +
+ +
+ )} + {/* Big Title */} +
+

{form.watch("bigTitle")}

+
+ {/* Sub Title */} +
+

{form.watch("subTitle")}

+
+ {/* Footer */} +
+

{form.watch("footer")}

+
+
+
+ + + +
+
+
+ + + + + + + +

Could cause your cover to be removed.

+
+
+
+
+ ( + + + + + + )} + /> +
+ + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> +
+ + + + + + + Gradient + + + Color + + + + Image + + + + ( + + + + {[...Array(30)].map((_, index) => ( + + ))} + + + + )} + /> + + + + + Coming soon + Color selection will be available soon. + + + + + + Coming soon + Image upload will be available soon. + + {/* */} + + +
+ + + + +

+ Built by{" "} + + Boostvolt + + . The source code is available on{" "} + + GitHub + + . +

+
+ +
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..aeafa41 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "@/styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/components/icons/apple-music.tsx b/components/icons/apple-music.tsx new file mode 100644 index 0000000..f4d4265 --- /dev/null +++ b/components/icons/apple-music.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import { forwardRef, memo, Ref, SVGProps } from "react" + +const AppleMusic = (props: SVGProps, ref: Ref) => ( + + + +) + +const ForwardRef = forwardRef(AppleMusic) +const Memo = memo(ForwardRef) + +export default Memo diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..bafb34b --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { ThemeProviderProps } from "next-themes/dist/types" +import * as React from "react" + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..e868ff1 --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,19 @@ +"use client" + +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" +import * as React from "react" + +import { Button } from "@/components/ui/button" + +export function ThemeToggle() { + const { setTheme, theme } = useTheme() + + return ( + + ) +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..dbfbf5c --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,43 @@ +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..c1b9f80 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,40 @@ +"use client" + +import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..5242fe2 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,47 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..4b08e31 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,41 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) =>

+) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) =>
+) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..deafd08 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,181 @@ +"use client" + +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..bd7d2d1 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,129 @@ +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import * as React from "react" +import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form" + +import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext({} as FormItemContextValue) + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) + } +) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return