Compare commits
5 Commits
f66ca69c15
...
b2ef80d58d
Author | SHA1 | Date | |
---|---|---|---|
b2ef80d58d | |||
ee3faad379 | |||
b45e815f1d | |||
290f97dd95 | |||
52d62f831f |
@ -13,13 +13,16 @@
|
||||
"@boring.tools/schema": "workspace:*",
|
||||
"@boring.tools/ui": "workspace:*",
|
||||
"@clerk/clerk-react": "^5.9.4",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"@tanstack/react-router": "^1.58.15",
|
||||
"axios": "^1.7.7",
|
||||
"lucide-react": "^0.446.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"react-hook-form": "^7.53.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
|
@ -37,7 +37,6 @@ export const Navigation = () => {
|
||||
to={route.to}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary"
|
||||
activeProps={{ className: 'bg-muted text-primary' }}
|
||||
activeOptions={{ exact: true }}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.name}
|
||||
|
@ -28,7 +28,7 @@ export const useChangelogById = ({ id }: { id: string }) => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['changelogById', id],
|
||||
queryFn: async (): Promise<ReadOnlyDict<Changelog>> =>
|
||||
queryFn: async (): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: `changelog/${id}`,
|
||||
method: 'get',
|
||||
@ -44,7 +44,7 @@ export const useChangelogCreate = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
payload: ChangelogCreate,
|
||||
): Promise<ReadonlySet<Changelog>> =>
|
||||
): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: 'changelog',
|
||||
data: payload,
|
||||
@ -62,9 +62,7 @@ export const useChangelogRemove = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
}: { id: string }): Promise<ReadOnlyDict<Changelog>> =>
|
||||
mutationFn: async ({ id }: { id: string }): Promise<Readonly<Changelog>> =>
|
||||
await queryFetch({
|
||||
path: `changelog/${id}`,
|
||||
method: 'delete',
|
||||
|
@ -19,6 +19,8 @@ import { Route as rootRoute } from './routes/__root'
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
const UserIndexLazyImport = createFileRoute('/user/')()
|
||||
const ChangelogIndexLazyImport = createFileRoute('/changelog/')()
|
||||
const ChangelogCreateLazyImport = createFileRoute('/changelog/create')()
|
||||
const ChangelogIdLazyImport = createFileRoute('/changelog/$id')()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@ -39,6 +41,18 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||
import('./routes/changelog/index.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||
path: '/changelog/create',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/changelog/create.lazy').then((d) => d.Route),
|
||||
)
|
||||
|
||||
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||
path: '/changelog/$id',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/changelog/$id.lazy').then((d) => d.Route))
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@ -50,6 +64,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/$id': {
|
||||
id: '/changelog/$id'
|
||||
path: '/changelog/$id'
|
||||
fullPath: '/changelog/$id'
|
||||
preLoaderRoute: typeof ChangelogIdLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/create': {
|
||||
id: '/changelog/create'
|
||||
path: '/changelog/create'
|
||||
fullPath: '/changelog/create'
|
||||
preLoaderRoute: typeof ChangelogCreateLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/changelog/': {
|
||||
id: '/changelog/'
|
||||
path: '/changelog'
|
||||
@ -71,12 +99,16 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog': typeof ChangelogIndexLazyRoute
|
||||
'/user': typeof UserIndexLazyRoute
|
||||
}
|
||||
@ -84,27 +116,44 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexLazyRoute
|
||||
'/changelog/$id': typeof ChangelogIdLazyRoute
|
||||
'/changelog/create': typeof ChangelogCreateLazyRoute
|
||||
'/changelog/': typeof ChangelogIndexLazyRoute
|
||||
'/user/': typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/changelog' | '/user'
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/changelog'
|
||||
| '/user'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/changelog' | '/user'
|
||||
id: '__root__' | '/' | '/changelog/' | '/user/'
|
||||
to: '/' | '/changelog/$id' | '/changelog/create' | '/changelog' | '/user'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/changelog/$id'
|
||||
| '/changelog/create'
|
||||
| '/changelog/'
|
||||
| '/user/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexLazyRoute: typeof IndexLazyRoute
|
||||
ChangelogIdLazyRoute: typeof ChangelogIdLazyRoute
|
||||
ChangelogCreateLazyRoute: typeof ChangelogCreateLazyRoute
|
||||
ChangelogIndexLazyRoute: typeof ChangelogIndexLazyRoute
|
||||
UserIndexLazyRoute: typeof UserIndexLazyRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexLazyRoute: IndexLazyRoute,
|
||||
ChangelogIdLazyRoute: ChangelogIdLazyRoute,
|
||||
ChangelogCreateLazyRoute: ChangelogCreateLazyRoute,
|
||||
ChangelogIndexLazyRoute: ChangelogIndexLazyRoute,
|
||||
UserIndexLazyRoute: UserIndexLazyRoute,
|
||||
}
|
||||
@ -122,6 +171,8 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/changelog/$id",
|
||||
"/changelog/create",
|
||||
"/changelog/",
|
||||
"/user/"
|
||||
]
|
||||
@ -129,6 +180,12 @@ export const routeTree = rootRoute
|
||||
"/": {
|
||||
"filePath": "index.lazy.tsx"
|
||||
},
|
||||
"/changelog/$id": {
|
||||
"filePath": "changelog/$id.lazy.tsx"
|
||||
},
|
||||
"/changelog/create": {
|
||||
"filePath": "changelog/create.lazy.tsx"
|
||||
},
|
||||
"/changelog/": {
|
||||
"filePath": "changelog/index.lazy.tsx"
|
||||
},
|
||||
|
33
apps/app/src/routes/changelog/$id.lazy.tsx
Normal file
33
apps/app/src/routes/changelog/$id.lazy.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Button } from '@boring.tools/ui'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useChangelogById } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const { id } = Route.useParams()
|
||||
const { data, error, isPending, refetch } = useChangelogById({ id })
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-32 flex-col">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
<p>Please try again later</p>
|
||||
|
||||
<Button onClick={() => refetch()}>Retry</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{!isPending && data && (
|
||||
<div>
|
||||
<h1 className="text-3xl">{data.title}</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/changelog/$id')({
|
||||
component: Component,
|
||||
})
|
117
apps/app/src/routes/changelog/create.lazy.tsx
Normal file
117
apps/app/src/routes/changelog/create.lazy.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { ChangelogCreateInput } from '@boring.tools/schema'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@boring.tools/ui'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import type { z } from 'zod'
|
||||
import { useChangelogCreate } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const navigate = useNavigate({ from: '/changelog/create' })
|
||||
const changelogCreate = useChangelogCreate()
|
||||
const form = useForm<z.infer<typeof ChangelogCreateInput>>({
|
||||
resolver: zodResolver(ChangelogCreateInput),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
description: '',
|
||||
isSemver: true,
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof ChangelogCreateInput>) => {
|
||||
changelogCreate.mutate(values, {
|
||||
onSuccess(data) {
|
||||
navigate({ to: '/changelog/$id', params: { id: data.id } })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">New changelog</h1>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 max-w-screen-md"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="My changelog" {...field} autoFocus />
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Some details about the changelog..."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>{' '}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isSemver"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Using Semver</FormLabel>
|
||||
<FormDescription>
|
||||
If this changelog is following the{' '}
|
||||
<a
|
||||
href="https://semver.org/lang/de/"
|
||||
className="text-emerald-700"
|
||||
>
|
||||
semantic versioning?
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Create</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createLazyFileRoute('/changelog/create')({
|
||||
component: Component,
|
||||
})
|
@ -1,27 +1,67 @@
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@boring.tools/ui'
|
||||
import { Link, createLazyFileRoute } from '@tanstack/react-router'
|
||||
import { PlusCircleIcon } from 'lucide-react'
|
||||
import { useChangelogList } from '../../hooks/useChangelog'
|
||||
|
||||
const Component = () => {
|
||||
const { data, error, isPending } = useChangelogList()
|
||||
const { data, error, isPending, refetch } = useChangelogList()
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center mt-32 flex-col">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
<p>Please try again later</p>
|
||||
|
||||
<Button onClick={() => refetch()}>Retry</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<h1 className="text-3xl">Changelogs</h1>
|
||||
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return <div key={changelog.id}>{changelog.title}</div>
|
||||
})}
|
||||
<div className="flex gap-10 w-full">
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.map((changelog) => {
|
||||
return (
|
||||
<Link
|
||||
to="/changelog/$id"
|
||||
params={{ id: changelog.id }}
|
||||
key={changelog.id}
|
||||
>
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>{changelog.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center flex-col">
|
||||
<span>Versions: {changelog.computed.versionCount}</span>
|
||||
|
||||
<span>Commits: {changelog.computed.commitCount}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
<Link to="/changelog/create">
|
||||
<Card className="max-w-56 min-w-56 w-full h-36 hover:border-emerald-700 transition">
|
||||
<CardHeader className="flex items-center justify-center">
|
||||
<CardTitle>New Changelog</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center">
|
||||
<PlusCircleIcon strokeWidth={1.5} className="w-10 h-10" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ export const ChangelogCreateInput = z
|
||||
.object({
|
||||
title: z.string().min(3, 'Title must contain at least 3 charachters.'),
|
||||
description: z.string(),
|
||||
isSemver: z.boolean().default(true),
|
||||
})
|
||||
.openapi({
|
||||
required: ['title', 'userId'],
|
||||
|
@ -5,17 +5,22 @@
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.446.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
|
28
packages/ui/src/checkbox.tsx
Normal file
28
packages/ui/src/checkbox.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './lib/cn'
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn('flex items-center justify-center text-current')}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
176
packages/ui/src/form.tsx
Normal file
176
packages/ui/src/form.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import type * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Controller,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form'
|
||||
|
||||
import { Label } from './label'
|
||||
import { cn } from './lib/cn'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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 <FormField>')
|
||||
}
|
||||
|
||||
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<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-[0.8rem] text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
@ -8,3 +8,7 @@ export * from './card'
|
||||
export * from './input'
|
||||
export * from './sheet'
|
||||
export * from './accordion'
|
||||
export * from './label'
|
||||
export * from './form'
|
||||
export * from './checkbox'
|
||||
export * from './textarea'
|
||||
|
24
packages/ui/src/label.tsx
Normal file
24
packages/ui/src/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './lib/cn'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
24
packages/ui/src/textarea.tsx
Normal file
24
packages/ui/src/textarea.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from './lib/cn'
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export { Textarea }
|
Loading…
Reference in New Issue
Block a user