Change mode toggle. Add type edit

This commit is contained in:
2025-07-06 10:25:20 +02:00
parent a0b515366a
commit c6a819c11b
8 changed files with 233 additions and 6 deletions

View File

@ -0,0 +1,46 @@
"use server"
import prisma from "@/lib/prisma"
import { commissionTypeSchema } from "@/schemas/commissionType"
import * as z from "zod/v4"
export async function updateCommissionType(
id: string,
rawData: z.infer<typeof commissionTypeSchema>
) {
const data = commissionTypeSchema.parse(rawData)
const updated = await prisma.commissionType.update({
where: { id },
data: {
name: data.name,
description: data.description,
options: {
deleteMany: {},
create: data.options?.map((opt, index) => ({
option: { connect: { id: opt.optionId } },
price: opt.price ?? null,
pricePercent: opt.pricePercent ?? null,
priceRange: opt.priceRange ?? null,
sortIndex: index,
})),
},
extras: {
deleteMany: {},
create: data.extras?.map((ext, index) => ({
extra: { connect: { id: ext.extraId } },
price: ext.price ?? null,
pricePercent: ext.pricePercent ?? null,
priceRange: ext.priceRange ?? null,
sortIndex: index,
})),
},
},
include: {
options: true,
extras: true,
},
})
return updated
}

View File

@ -0,0 +1,34 @@
import EditTypeForm from "@/components/items/commissions/types/EditTypeForm";
import prisma from "@/lib/prisma";
export default async function CommissionTypesEditPage({ params }: { params: { id: string } }) {
const { id } = await params;
const commissionType = await prisma.commissionType.findUnique({
where: {
id,
},
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
},
})
const options = await prisma.commissionOption.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
});
const extras = await prisma.commissionExtra.findMany({
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})
if (!commissionType) {
return <div>Type not found</div>
}
return (
<div>
<div className="flex gap-4 justify-between pb-8">
<h1 className="text-2xl font-bold mb-4">New Commission Type</h1>
</div>
<EditTypeForm type={commissionType} allOptions={options} allExtras={extras} />
</div>
);
}

View File

@ -8,7 +8,7 @@ import { useEffect, useState } from "react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
const modes = ["light", "dark"] as const const modes = ["light", "dark"] as const
const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet", "default"] as const const accents = ["zinc", "red", "rose", "orange", "green", "blue", "yellow", "violet"] as const
const modeIcons = { const modeIcons = {
light: <Sun className="h-4 w-4" />, light: <Sun className="h-4 w-4" />,
@ -16,7 +16,7 @@ const modeIcons = {
} }
export default function ModeToggle() { export default function ModeToggle() {
const { setTheme, theme, resolvedTheme } = useTheme() const { setTheme, theme } = useTheme()
const [mode, setMode] = useState("dark") const [mode, setMode] = useState("dark")
const [accent, setAccent] = useState("violet") const [accent, setAccent] = useState("violet")
@ -33,6 +33,17 @@ export default function ModeToggle() {
setTheme(fullTheme) setTheme(fullTheme)
} }
const accentColorMap: Record<string, string> = {
zinc: "text-zinc-600",
red: "text-red-600",
rose: "text-rose-600",
orange: "text-orange-600",
green: "text-green-600",
blue: "text-blue-600",
yellow: "text-yellow-600",
violet: "text-violet-600",
}
return ( return (
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
<Select <Select
@ -71,7 +82,7 @@ export default function ModeToggle() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{accents.map((a) => ( {accents.map((a) => (
<SelectItem key={a} value={a} className={cn(`text-${a}-600`)}> <SelectItem key={a} value={a} className={cn(accentColorMap[a])}>
{a.charAt(0).toUpperCase() + a.slice(1)} {a.charAt(0).toUpperCase() + a.slice(1)}
</SelectItem> </SelectItem>
))} ))}

View File

@ -12,6 +12,11 @@ export default function TopNav() {
<Link href="/">Home</Link> <Link href="/">Home</Link>
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/items/commissions/types">CommissionTypes</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
); );

View File

@ -0,0 +1,105 @@
"use client"
import { updateCommissionType } from "@/actions/items/commissions/types/updateType";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
import { commissionTypeSchema } from "@/schemas/commissionType";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod/v4";
import { CommissionExtraField } from "./form/CommissionExtraField";
import { CommissionOptionField } from "./form/CommissionOptionField";
type CommissionTypeWithConnections = CommissionType & {
options: (CommissionTypeOption & { option: CommissionOption })[]
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
}
type Props = {
type: CommissionTypeWithConnections
allOptions: CommissionOption[],
allExtras: CommissionExtra[],
}
export default function EditTypeForm({ type, allOptions, allExtras }: Props) {
const router = useRouter();
const form = useForm<z.infer<typeof commissionTypeSchema>>({
resolver: zodResolver(commissionTypeSchema),
defaultValues: {
name: type.name,
description: type.description ?? "",
options: type.options.map((o) => ({
optionId: o.optionId,
price: o.price ?? undefined,
pricePercent: o.pricePercent ?? undefined,
priceRange: o.priceRange ?? undefined,
})),
extras: type.extras.map((e) => ({
extraId: e.extraId,
price: e.price ?? undefined,
pricePercent: e.pricePercent ?? undefined,
priceRange: e.priceRange ?? undefined,
})),
},
})
async function onSubmit(values: z.infer<typeof commissionTypeSchema>) {
try {
await updateCommissionType(type.id, values)
toast.success("Commission type updated.")
router.push("/items/commissions/types")
} catch (err) {
console.error(err)
toast("Failed to create commission type.")
}
}
return (
<div className="flex flex-col gap-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>The name of the commission type.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Optional description.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<CommissionOptionField options={allOptions} />
<CommissionExtraField extras={allExtras} />
<div className="flex flex-col gap-4">
<Button type="submit">Submit</Button>
<Button type="reset" variant="secondary" onClick={() => router.back()}>Cancel</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -2,7 +2,7 @@
import { updateCommissionTypeSortOrder } from "@/actions/items/commissions/types/updateCommissionTypeSortOrder"; import { updateCommissionTypeSortOrder } from "@/actions/items/commissions/types/updateCommissionTypeSortOrder";
import SortableItemCard from "@/components/drag/SortableItemCard"; import SortableItemCard from "@/components/drag/SortableItemCard";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"; import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma";
import { import {
closestCenter, closestCenter,
@ -17,6 +17,8 @@ import {
rectSortingStrategy, rectSortingStrategy,
SortableContext SortableContext
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { PencilIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type CommissionTypeWithItems = CommissionType & { type CommissionTypeWithItems = CommissionType & {
@ -66,6 +68,7 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[]
<CardHeader> <CardHeader>
<CardTitle className="text-xl truncate">{type.name}</CardTitle> <CardTitle className="text-xl truncate">{type.name}</CardTitle>
<CardDescription>{type.description}</CardDescription> <CardDescription>{type.description}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col justify-start gap-4"> <CardContent className="flex flex-col justify-start gap-4">
<div> <div>
@ -103,6 +106,15 @@ export default function ListTypes({ types }: { types: CommissionTypeWithItems[]
</ul> </ul>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex items-center justify-between">
<Link
href={`/items/commissions/types/${type.id}/edit`}
className="text-sm text-primary hover:underline flex items-center gap-1"
>
<PencilIcon className="h-4 w-4" />
Edit
</Link>
</CardFooter>
</Card> </Card>
</SortableItemCard > </SortableItemCard >
))} ))}

View File

@ -24,7 +24,7 @@ import {
SortableContext, SortableContext,
verticalListSortingStrategy verticalListSortingStrategy
} from "@dnd-kit/sortable" } from "@dnd-kit/sortable"
import { useState } from "react" import { useEffect, useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form" import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable" import { ComboboxCreateable } from "./ComboboxCreateable"
@ -33,12 +33,17 @@ type Props = {
} }
export function CommissionExtraField({ extras: initialExtras }: Props) { export function CommissionExtraField({ extras: initialExtras }: Props) {
const [mounted, setMounted] = useState(false)
const { control } = useFormContext() const { control } = useFormContext()
const { fields, append, remove, move } = useFieldArray({ const { fields, append, remove, move } = useFieldArray({
control, control,
name: "extras", name: "extras",
}) })
useEffect(() => {
setMounted(true)
}, [])
const [extras, setExtras] = useState(initialExtras) const [extras, setExtras] = useState(initialExtras)
const sensors = useSensors(useSensor(PointerSensor)) const sensors = useSensors(useSensor(PointerSensor))
@ -58,6 +63,8 @@ export function CommissionExtraField({ extras: initialExtras }: Props) {
} }
} }
if (!mounted) return null
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Extras</h3> <h3 className="text-lg font-semibold">Extras</h3>

View File

@ -24,7 +24,7 @@ import {
SortableContext, SortableContext,
verticalListSortingStrategy verticalListSortingStrategy
} from "@dnd-kit/sortable" } from "@dnd-kit/sortable"
import { useState } from "react" import { useEffect, useState } from "react"
import { useFieldArray, useFormContext } from "react-hook-form" import { useFieldArray, useFormContext } from "react-hook-form"
import { ComboboxCreateable } from "./ComboboxCreateable" import { ComboboxCreateable } from "./ComboboxCreateable"
@ -33,12 +33,17 @@ type Props = {
} }
export function CommissionOptionField({ options: initialOptions }: Props) { export function CommissionOptionField({ options: initialOptions }: Props) {
const [mounted, setMounted] = useState(false)
const { control } = useFormContext() const { control } = useFormContext()
const { fields, append, remove, move } = useFieldArray({ const { fields, append, remove, move } = useFieldArray({
control, control,
name: "options", name: "options",
}) })
useEffect(() => {
setMounted(true)
}, [])
const [options, setOptions] = useState(initialOptions) const [options, setOptions] = useState(initialOptions)
const sensors = useSensors(useSensor(PointerSensor)) const sensors = useSensors(useSensor(PointerSensor))
@ -58,6 +63,8 @@ export function CommissionOptionField({ options: initialOptions }: Props) {
} }
} }
if (!mounted) return null
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-semibold">Options</h3> <h3 className="text-lg font-semibold">Options</h3>