From 72a11f92b2855f0fc6e525f891bc96b15f2ee8fe Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Fri, 6 Sep 2024 06:28:50 -0400 Subject: [PATCH] init --- .eslintrc.cjs | 136 +-- .prettierrc | 10 + README.md | 6 +- app/components/Link.tsx | 13 + app/components/cards/Card.tsx | 20 + .../cards/employment/EmployeeList.tsx | 77 ++ app/components/cards/employment/index.tsx | 11 + app/components/cards/sample/index.tsx | 15 + app/components/mode-toggle.tsx | 34 + app/components/sidebar/SidebarProfile.tsx | 22 + app/components/sidebar/index.tsx | 39 + app/components/title/index.tsx | 16 + app/components/ui/avatar.tsx | 48 + app/components/ui/button.tsx | 57 + app/components/ui/context-menu.tsx | 202 ++++ app/components/ui/dialog.tsx | 120 +++ app/components/ui/dropdown-menu.tsx | 206 ++++ app/components/ui/form.tsx | 176 +++ app/components/ui/label.tsx | 24 + app/components/ui/menubar.tsx | 238 +++++ app/components/ui/resizable.tsx | 43 + app/components/ui/scroll-area.tsx | 46 + app/components/ui/separator.tsx | 29 + app/components/ui/sheet.tsx | 138 +++ app/components/ui/skeleton.tsx | 15 + app/components/ui/table.tsx | 120 +++ app/entry.client.tsx | 18 +- app/entry.server.tsx | 222 ++-- app/lib/utils.ts | 6 + app/root.tsx | 102 +- app/routes/_index.tsx | 69 +- app/routes/action.set-theme.ts | 4 + app/sessions.server.tsx | 19 + app/tailwind.css | 72 ++ components.json | 20 + package.json | 104 +- pnpm-lock.yaml | 999 ++++++++++++++++++ postcss.config.js | 8 +- public/logo.svg | 1 + tailwind.config.ts | 61 +- tsconfig.json | 58 +- vite.config.ts | 26 +- 42 files changed, 3298 insertions(+), 352 deletions(-) create mode 100644 .prettierrc create mode 100644 app/components/Link.tsx create mode 100644 app/components/cards/Card.tsx create mode 100644 app/components/cards/employment/EmployeeList.tsx create mode 100644 app/components/cards/employment/index.tsx create mode 100644 app/components/cards/sample/index.tsx create mode 100644 app/components/mode-toggle.tsx create mode 100644 app/components/sidebar/SidebarProfile.tsx create mode 100644 app/components/sidebar/index.tsx create mode 100644 app/components/title/index.tsx create mode 100644 app/components/ui/avatar.tsx create mode 100644 app/components/ui/button.tsx create mode 100644 app/components/ui/context-menu.tsx create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/dropdown-menu.tsx create mode 100644 app/components/ui/form.tsx create mode 100644 app/components/ui/label.tsx create mode 100644 app/components/ui/menubar.tsx create mode 100644 app/components/ui/resizable.tsx create mode 100644 app/components/ui/scroll-area.tsx create mode 100644 app/components/ui/separator.tsx create mode 100644 app/components/ui/sheet.tsx create mode 100644 app/components/ui/skeleton.tsx create mode 100644 app/components/ui/table.tsx create mode 100644 app/lib/utils.ts create mode 100644 app/routes/action.set-theme.ts create mode 100644 app/sessions.server.tsx create mode 100644 components.json create mode 100644 public/logo.svg diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 4f6f59e..47bf936 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,79 +6,79 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, + root: true, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], + env: { + browser: true, + commonjs: true, + es6: true + }, + ignorePatterns: ['!**/.server', '!**/.client'], - // Base config - extends: ["eslint:recommended"], + // Base config + extends: ['eslint:recommended'], - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", + overrides: [ + // React + { + files: ['**/*.{js,jsx,ts,tsx}'], + plugins: ['react', 'jsx-a11y'], + extends: [ + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended' + ], + settings: { + react: { + version: 'detect' + }, + formComponents: ['Form'], + linkComponents: [ + { name: 'Link', linkAttribute: 'to' }, + { name: 'NavLink', linkAttribute: 'to' } + ], + 'import/resolver': { + typescript: {} + } + } }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, + // Typescript + { + files: ['**/*.{ts,tsx}'], + plugins: ['@typescript-eslint', 'import'], + parser: '@typescript-eslint/parser', + settings: { + 'import/internal-regex': '^~/', + 'import/resolver': { + node: { + extensions: ['.ts', '.tsx'] + }, + typescript: { + alwaysTryTypes: true + } + } + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript' + ] }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], + // Node + { + files: ['.eslintrc.cjs'], + env: { + node: true + } + } + ] }; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f3c2d7c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "lf", + "tabWidth": 4, + "bracketSpacing": true, + "bracketSameLine": false +} diff --git a/README.md b/README.md index 6c4d216..ffa46e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Welcome to Remix! -- 📖 [Remix docs](https://remix.run/docs) +- 📖 [Remix docs](https://remix.run/docs) ## Development @@ -32,8 +32,8 @@ If you're familiar with deploying Node applications, the built-in Remix app serv Make sure to deploy the output of `npm run build` -- `build/server` -- `build/client` +- `build/server` +- `build/client` ## Styling diff --git a/app/components/Link.tsx b/app/components/Link.tsx new file mode 100644 index 0000000..aa1b152 --- /dev/null +++ b/app/components/Link.tsx @@ -0,0 +1,13 @@ +import { LinkProps } from '@remix-run/react'; + +export default function Link(props: LinkProps & { to: string }) { + return ( + + {props.children} + + ); +} diff --git a/app/components/cards/Card.tsx b/app/components/cards/Card.tsx new file mode 100644 index 0000000..bdfc273 --- /dev/null +++ b/app/components/cards/Card.tsx @@ -0,0 +1,20 @@ +import { ResizablePanel } from '../ui/resizable'; + +export default function Card({ children }: { children: React.ReactNode }) { + return ( + +
{children}
+
+ ); +} + +export function CardHeader({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/components/cards/employment/EmployeeList.tsx b/app/components/cards/employment/EmployeeList.tsx new file mode 100644 index 0000000..618e92a --- /dev/null +++ b/app/components/cards/employment/EmployeeList.tsx @@ -0,0 +1,77 @@ +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '~/components/ui/table'; +import { Skeleton } from '~/components/ui/skeleton'; + +interface Employee { + id: number; + displayName: string; + tokenContainerId: string; + position: string; +} + +export default function EmployeeList() { + // Get list of employees from API + const [employees, setEmployees] = useState([] as Employee[]); + + useEffect(() => { + const fetchEmployees = async () => { + const response = await fetch( + 'https://xnl.hri7566.info/api/employment' + ); + const data = (await response.json()) as { + type: 'employment'; + count: number; + employees: Employee[]; + }; + setEmployees(data.employees); + }; + fetchEmployees(); + }, []); + + return ( + + + + ID + Display Name + Token Container ID + Position + + + + {employees.length === 0 ? ( + + + + + + + + + + + + + + + ) : ( + employees.map(employee => ( + + {employee.id} + {employee.displayName} + {employee.tokenContainerId} + {employee.position} + + )) + )} + +
+ ); +} diff --git a/app/components/cards/employment/index.tsx b/app/components/cards/employment/index.tsx new file mode 100644 index 0000000..e5b0a6c --- /dev/null +++ b/app/components/cards/employment/index.tsx @@ -0,0 +1,11 @@ +import Card, { CardHeader } from '../Card'; +import EmployeeList from './EmployeeList'; + +export default function EmploymentCard() { + return ( + + Employees + + + ); +} diff --git a/app/components/cards/sample/index.tsx b/app/components/cards/sample/index.tsx new file mode 100644 index 0000000..0754b3c --- /dev/null +++ b/app/components/cards/sample/index.tsx @@ -0,0 +1,15 @@ +import Card, { CardHeader } from '../Card'; + +export default function SampleCard() { + return ( + + Sample +
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Hic + sunt nobis quo, dignissimos cupiditate minus aperiam alias, + eaque laboriosam qui laudantium maxime accusamus nesciunt ad + voluptatum doloremque esse eius exercitationem! +
+
+ ); +} diff --git a/app/components/mode-toggle.tsx b/app/components/mode-toggle.tsx new file mode 100644 index 0000000..4d5f1b7 --- /dev/null +++ b/app/components/mode-toggle.tsx @@ -0,0 +1,34 @@ +import { Moon, Sun } from 'lucide-react'; +import { Theme, useTheme } from 'remix-themes'; + +import { Button } from './ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from './ui/dropdown-menu'; + +export function ModeToggle() { + const [, setTheme] = useTheme(); + + return ( + + + + + + setTheme(Theme.LIGHT)}> + Light + + setTheme(Theme.DARK)}> + Dark + + + + ); +} diff --git a/app/components/sidebar/SidebarProfile.tsx b/app/components/sidebar/SidebarProfile.tsx new file mode 100644 index 0000000..f2e8a97 --- /dev/null +++ b/app/components/sidebar/SidebarProfile.tsx @@ -0,0 +1,22 @@ +import { Avatar, AvatarImage, AvatarFallback } from '~/components/ui/avatar'; +import { LogOut } from 'lucide-react'; +import { Button } from '~/components/ui/button'; + +export default function SidebarProfile() { + return ( + + ); +} diff --git a/app/components/sidebar/index.tsx b/app/components/sidebar/index.tsx new file mode 100644 index 0000000..5ec4478 --- /dev/null +++ b/app/components/sidebar/index.tsx @@ -0,0 +1,39 @@ +import Link from '../Link'; +import SidebarProfile from './SidebarProfile'; + +export default function Sidebar() { + return ( + + ); +} diff --git a/app/components/title/index.tsx b/app/components/title/index.tsx new file mode 100644 index 0000000..5d4e364 --- /dev/null +++ b/app/components/title/index.tsx @@ -0,0 +1,16 @@ +import { ModeToggle } from '../mode-toggle'; + +export default function Title(props: { title: string }) { + return ( +
+
+

{props.title}

+
+ + +
+ ); +} diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx new file mode 100644 index 0000000..706f177 --- /dev/null +++ b/app/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +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/app/components/ui/button.tsx b/app/components/ui/button.tsx new file mode 100644 index 0000000..f0d10c2 --- /dev/null +++ b/app/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '~/lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap 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/app/components/ui/context-menu.tsx b/app/components/ui/context-menu.tsx new file mode 100644 index 0000000..f25f2f0 --- /dev/null +++ b/app/components/ui/context-menu.tsx @@ -0,0 +1,202 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "~/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..5f064f1 --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "~/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..a4d6abd --- /dev/null +++ b/app/components/ui/dropdown-menu.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon +} from '@radix-ui/react-icons'; + +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/app/components/ui/form.tsx b/app/components/ui/form.tsx new file mode 100644 index 0000000..69f6b15 --- /dev/null +++ b/app/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "~/lib/utils" +import { Label } from "~/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, 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< + HTMLDivElement, + React.HTMLAttributes +>(({ 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 ( +