add dynamically fetched checkboxes

This commit is contained in:
andrzej 2024-06-13 12:11:09 +02:00
parent f3e5233171
commit f503647469
9 changed files with 359 additions and 100 deletions

View File

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.6.0", "@hookform/resolvers": "^3.6.0",
"@prisma/client": "^5.15.0", "@prisma/client": "^5.15.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
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 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -1,14 +1,12 @@
"use server"
import prisma from "./db" import prisma from "./db"
export async function getStories() { export async function getStories() {
"use server"
return prisma.story.findMany() return prisma.story.findMany()
} }
export async function getPubs() { export async function getPubs() {
"use server"
return prisma.pub.findMany() return prisma.pub.findMany()
} }
export async function getGenres() { export async function getGenres() {
"use server"
return prisma.genre.findMany() return prisma.genre.findMany()
} }

View File

@ -1,5 +1,5 @@
"use server"
import SubmissionForm from "app/ui/forms/sub"; import SubmissionForm from "app/ui/forms/sub";
export default async function Page() {
export default function Page() {
return <SubmissionForm /> return <SubmissionForm />
} }

View File

@ -1,14 +1,7 @@
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import FancyForm from "app/ui/forms/fancyForm" import FancyForm from "app/ui/forms/fancyForm"
import { getGenres } from "app/lib/get"
export default function Page() { export default async function Page() {
return <FancyForm /> const genres = await getGenres()
return <FancyForm genres={genres} />
} }

View File

@ -616,25 +616,21 @@ body {
z-index: 50; z-index: 50;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.-mx-1 { .-mx-1 {
margin-left: -0.25rem; margin-left: -0.25rem;
margin-right: -0.25rem; margin-right: -0.25rem;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-1 { .my-1 {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.mt-4 {
margin-top: 1rem;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
@ -643,6 +639,18 @@ body {
margin-left: auto; margin-left: auto;
} }
.mt-4 {
margin-top: 1rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.flex { .flex {
display: flex; display: flex;
} }
@ -651,20 +659,12 @@ body {
display: inline-flex; display: inline-flex;
} }
.table {
display: table;
}
.\!table { .\!table {
display: table !important; display: table !important;
} }
.h-12 { .table {
height: 3rem; display: table;
}
.h-24 {
height: 6rem;
} }
.h-10 { .h-10 {
@ -675,18 +675,18 @@ body {
height: 2.75rem; height: 2.75rem;
} }
.h-9 { .h-12 {
height: 2.25rem; height: 3rem;
}
.h-4 {
height: 1rem;
} }
.h-2 { .h-2 {
height: 0.5rem; height: 0.5rem;
} }
.h-24 {
height: 6rem;
}
.h-3 { .h-3 {
height: 0.75rem; height: 0.75rem;
} }
@ -695,22 +695,22 @@ body {
height: 0.875rem; height: 0.875rem;
} }
.h-4 {
height: 1rem;
}
.h-9 {
height: 2.25rem;
}
.h-px { .h-px {
height: 1px; height: 1px;
} }
.w-full {
width: 100%;
}
.w-10 { .w-10 {
width: 2.5rem; width: 2.5rem;
} }
.w-4 {
width: 1rem;
}
.w-2 { .w-2 {
width: 0.5rem; width: 0.5rem;
} }
@ -723,6 +723,18 @@ body {
width: 0.875rem; width: 0.875rem;
} }
.w-4 {
width: 1rem;
}
.w-full {
width: 100%;
}
.w-\[340px\] {
width: 340px;
}
.min-w-\[8rem\] { .min-w-\[8rem\] {
min-width: 8rem; min-width: 8rem;
} }
@ -731,6 +743,10 @@ body {
max-width: 24rem; max-width: 24rem;
} }
.shrink-0 {
flex-shrink: 0;
}
.caption-bottom { .caption-bottom {
caption-side: bottom; caption-side: bottom;
} }
@ -745,6 +761,14 @@ body {
user-select: none; user-select: none;
} }
.flex-row {
flex-direction: row;
}
.items-start {
align-items: flex-start;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@ -775,6 +799,18 @@ body {
margin-bottom: calc(2rem * var(--tw-space-y-reverse)); margin-bottom: calc(2rem * var(--tw-space-y-reverse));
} }
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-y-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0px * var(--tw-space-y-reverse));
}
.overflow-auto { .overflow-auto {
overflow: auto; overflow: auto;
} }
@ -811,8 +847,8 @@ body {
border-color: hsl(var(--input)); border-color: hsl(var(--input));
} }
.bg-muted\/50 { .border-primary {
background-color: hsl(var(--muted) / 0.5); border-color: hsl(var(--primary));
} }
.bg-background { .bg-background {
@ -823,6 +859,18 @@ body {
background-color: hsl(var(--destructive)); background-color: hsl(var(--destructive));
} }
.bg-muted {
background-color: hsl(var(--muted));
}
.bg-muted\/50 {
background-color: hsl(var(--muted) / 0.5);
}
.bg-popover {
background-color: hsl(var(--popover));
}
.bg-primary { .bg-primary {
background-color: hsl(var(--primary)); background-color: hsl(var(--primary));
} }
@ -831,34 +879,26 @@ body {
background-color: hsl(var(--secondary)); background-color: hsl(var(--secondary));
} }
.bg-muted { .bg-slate-950 {
background-color: hsl(var(--muted)); --tw-bg-opacity: 1;
} background-color: rgb(2 6 23 / var(--tw-bg-opacity));
.bg-popover {
background-color: hsl(var(--popover));
} }
.fill-current { .fill-current {
fill: currentColor; fill: currentColor;
} }
.p-4 {
padding: 1rem;
}
.p-1 { .p-1 {
padding: 0.25rem; padding: 0.25rem;
} }
.px-4 { .p-4 {
padding-left: 1rem; padding: 1rem;
padding-right: 1rem;
} }
.py-10 { .px-2 {
padding-top: 2.5rem; padding-left: 0.5rem;
padding-bottom: 2.5rem; padding-right: 0.5rem;
} }
.px-3 { .px-3 {
@ -866,26 +906,16 @@ body {
padding-right: 0.75rem; padding-right: 0.75rem;
} }
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.px-8 { .px-8 {
padding-left: 2rem; padding-left: 2rem;
padding-right: 2rem; padding-right: 2rem;
} }
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@ -896,6 +926,21 @@ body {
padding-bottom: 0.375rem; padding-bottom: 0.375rem;
} }
.py-10 {
padding-top: 2.5rem;
padding-bottom: 2.5rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.pl-8 { .pl-8 {
padding-left: 2rem; padding-left: 2rem;
} }
@ -931,6 +976,11 @@ body {
line-height: 1rem; line-height: 1rem;
} }
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.font-black { .font-black {
font-weight: 900; font-weight: 900;
} }
@ -943,6 +993,10 @@ body {
font-weight: 600; font-weight: 600;
} }
.font-normal {
font-weight: 400;
}
.capitalize { .capitalize {
text-transform: capitalize; text-transform: capitalize;
} }
@ -955,14 +1009,22 @@ body {
letter-spacing: 0.1em; letter-spacing: 0.1em;
} }
.text-muted-foreground { .text-destructive {
color: hsl(var(--muted-foreground)); color: hsl(var(--destructive));
} }
.text-destructive-foreground { .text-destructive-foreground {
color: hsl(var(--destructive-foreground)); color: hsl(var(--destructive-foreground));
} }
.text-muted-foreground {
color: hsl(var(--muted-foreground));
}
.text-popover-foreground {
color: hsl(var(--popover-foreground));
}
.text-primary { .text-primary {
color: hsl(var(--primary)); color: hsl(var(--primary));
} }
@ -975,12 +1037,13 @@ body {
color: hsl(var(--secondary-foreground)); color: hsl(var(--secondary-foreground));
} }
.text-popover-foreground { .text-current {
color: hsl(var(--popover-foreground)); color: currentColor;
} }
.text-destructive { .text-white {
color: hsl(var(--destructive)); --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.underline { .underline {
@ -1069,10 +1132,6 @@ body {
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground));
} }
.hover\:bg-muted\/50:hover {
background-color: hsl(var(--muted) / 0.5);
}
.hover\:bg-accent:hover { .hover\:bg-accent:hover {
background-color: hsl(var(--accent)); background-color: hsl(var(--accent));
} }
@ -1081,6 +1140,10 @@ body {
background-color: hsl(var(--destructive) / 0.9); background-color: hsl(var(--destructive) / 0.9);
} }
.hover\:bg-muted\/50:hover {
background-color: hsl(var(--muted) / 0.5);
}
.hover\:bg-primary\/90:hover { .hover\:bg-primary\/90:hover {
background-color: hsl(var(--primary) / 0.9); background-color: hsl(var(--primary) / 0.9);
} }
@ -1148,12 +1211,20 @@ body {
pointer-events: none; pointer-events: none;
} }
.data-\[state\=open\]\:bg-accent[data-state=open] {
background-color: hsl(var(--accent));
}
.data-\[state\=selected\]\:bg-muted[data-state=selected] { .data-\[state\=selected\]\:bg-muted[data-state=selected] {
background-color: hsl(var(--muted)); background-color: hsl(var(--muted));
} }
.data-\[state\=open\]\:bg-accent[data-state=open] { .data-\[state\=checked\]\:bg-primary[data-state=checked] {
background-color: hsl(var(--accent)); background-color: hsl(var(--primary));
}
.data-\[state\=checked\]\:text-primary-foreground[data-state=checked] {
color: hsl(var(--primary-foreground));
} }
.data-\[disabled\]\:opacity-50[data-disabled] { .data-\[disabled\]\:opacity-50[data-disabled] {

128
src/app/ui/forms/demo.tsx Normal file
View File

@ -0,0 +1,128 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { toast } from "@/components/ui/use-toast"
const items = [
{
id: "recents",
label: "Recents",
},
{
id: "home",
label: "Home",
},
{
id: "applications",
label: "Applications",
},
{
id: "desktop",
label: "Desktop",
},
{
id: "downloads",
label: "Downloads",
},
{
id: "documents",
label: "Documents",
},
] as const
const FormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "You have to select at least one item.",
}),
})
export function CheckboxReactHookFormMultiple() {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
items: ["recents", "home"],
},
})
function onSubmit(data: z.infer<typeof FormSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="items"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel className="text-base">Sidebar</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
{items.map((item) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
)
}}
/>
</FormControl>
<FormLabel className="text-sm font-normal">
{item.label}
</FormLabel>
</FormItem>
)
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}

View File

@ -15,23 +15,25 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
const formSchema = z.object({ const formSchema = z.object({
title: z.string().min(2).max(50), title: z.string().min(2).max(50),
word_count: z.number(), word_count: z.number(),
// genres: z.array() genres: z.object({ id: z.number(), name: z.string() }).array()
}) })
export default function FancyForm() {
export default function FancyForm({ genres }) {
// 1. Define your form. // 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
title: "", title: "",
word_count: 500, word_count: 0,
genres: genres
}, },
}) })
// 2. Define a submit handler. // 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values. // Do something with the form values.
@ -71,9 +73,44 @@ export default function FancyForm() {
name="genres" name="genres"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="mb-4">
<FormLabel>Genres</FormLabel>
<FormDescription>genres baby</FormDescription>
</div>
{genres.map((item) => (
<FormField
key={item.id}
control={form.control}
name="genres"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
)
}}
/>
</FormControl>
<FormLabel className="text-sm font-normal">
{item.name}
</FormLabel>
</FormItem>
)
}}
/>
))}
</FormItem> </FormItem>
)} )}
/> />

View File

@ -12,6 +12,7 @@
"noEmit": true, "noEmit": true,
"incremental": true, "incremental": true,
"module": "esnext", "module": "esnext",
"target": "es2017",
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,