Add custom inputs to commission types

This commit is contained in:
2025-07-07 20:53:20 +02:00
parent a870e54201
commit b2f0455804
11 changed files with 585 additions and 49 deletions

View File

@ -7,6 +7,7 @@ export default async function CommissionsPage() {
include: {
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
customInputs: { include: { customInput: true }, orderBy: { sortIndex: "asc" } },
},
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
})

View File

@ -160,6 +160,17 @@
@apply underline text-primary hover:text-primary/80;
}
.markdown li:has(input[type="checkbox"]) {
@apply list-none pl-0 items-start gap-2 mb-2;
}
.markdown input[type="checkbox"] {
@apply mt-1 h-4 w-4 shrink-0 rounded border border-border bg-background text-primary accent-primary cursor-default;
}
.markdown input[type="checkbox"]:checked + * {
@apply line-through text-muted-foreground;
}
@layer base {
* {

19
src/app/todo/page.tsx Normal file
View File

@ -0,0 +1,19 @@
import fs from "fs/promises";
import path from "path";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
export default async function TodoPage() {
const filePath = path.join(process.cwd(), "ToDo.md")
const content = await fs.readFile(filePath, "utf-8")
return (
<main className="markdown max-w-3xl mx-auto p-6">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
>
{content}
</ReactMarkdown>
</main>
);
}

View File

@ -11,9 +11,9 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"
import { CommissionCustomInput, CommissionExtra, CommissionOption, CommissionType, CommissionTypeCustomInput, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"
import { commissionOrderSchema } from "@/schemas/commissionOrder"
import { calculatePrice } from "@/utils/calculatePrice"
import { calculatePriceRange } from "@/utils/calculatePrice"
import { zodResolver } from "@hookform/resolvers/zod"
import Link from "next/link"
import { useMemo, useState } from "react"
@ -24,6 +24,7 @@ import { FileDropzone } from "./FileDropzone"
type CommissionTypeWithRelations = CommissionType & {
options: (CommissionTypeOption & { option: CommissionOption })[]
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
customInputs: (CommissionTypeCustomInput & { customInput: CommissionCustomInput })[]
}
type Props = {
@ -64,20 +65,13 @@ export function CommissionOrderForm({ types }: Props) {
[selectedType, extraIds]
)
const price = useMemo(() => {
if (!selectedOption) return 0
const base = calculatePrice(selectedOption, 0)
const extrasSum = selectedExtras.reduce(
(sum, ext) => sum + calculatePrice(ext, base),
0
)
return base + extrasSum
const [minPrice, maxPrice] = useMemo(() => {
return calculatePriceRange(selectedOption, selectedExtras)
}, [selectedOption, selectedExtras])
async function onSubmit(values: z.infer<typeof commissionOrderSchema>) {
console.log("Submit:", { ...values, files })
const { customFields, ...rest } = values
console.log("Submit:", { ...rest, customFields, files })
}
return (
@ -173,6 +167,8 @@ export function CommissionOrderForm({ types }: Props) {
</>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
@ -220,6 +216,54 @@ export function CommissionOrderForm({ types }: Props) {
)}
/>
{selectedType && selectedType.customInputs.length > 0 && (
<div className="space-y-4">
{selectedType.customInputs.map((input) => {
const name = `customFields.${input.customInput.name}`
return (
<FormField
key={input.id}
control={form.control}
name={name as `customFields.${string}`}
render={({ field }) => (
<FormItem>
<FormLabel>{input.label}</FormLabel>
<FormControl>
{input.inputType === "textarea" ? (
<Textarea {...field} rows={3} />
) : input.inputType === "number" ? (
<Input type="number" {...field} />
) : input.inputType === "checkbox" ? (
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
) : input.inputType === "date" ? (
<Input type="date" {...field} />
) : input.inputType === "select" ? (
// Placeholder select populate with options if needed
<select
{...field}
className="border rounded px-2 py-1 w-full"
>
<option value="">Please select</option>
<option value="example1">Example 1</option>
<option value="example2">Example 2</option>
</select>
) : (
<Input {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)
})}
</div>
)}
<FormItem>
<FormLabel>Reference Images</FormLabel>
<FormControl>
@ -237,7 +281,10 @@ export function CommissionOrderForm({ types }: Props) {
</FormItem>
<div className="text-lg font-semibold">
Estimated Price: {price.toFixed(2)}
Estimated Price:{" "}
{minPrice === maxPrice
? `${minPrice.toFixed(2)}`
: `${minPrice.toFixed(2)} ${maxPrice.toFixed(2)}`}
</div>
<div className="text-muted-foreground">

View File

@ -22,6 +22,11 @@ export default function TopNav() {
<Link href="/tos">Terms of Service</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/todo">todo (temp)</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);

View File

@ -4,6 +4,7 @@ export const commissionOrderSchema = z.object({
typeId: z.string().min(1, "Please select a type"),
optionId: z.string().min(1, "Please choose a base option"),
extraIds: z.array(z.string()).optional(),
customFields: z.record(z.string(), z.any()).optional(),
customerName: z.string().min(2, "Enter your name"),
customerEmail: z.string().email("Invalid email"),
message: z.string().min(5, "Please describe what you want"),

View File

@ -1,36 +1,3 @@
// export function calculatePrice(
// option: { price?: number; pricePercent?: number; priceRange?: string },
// extras: { price?: number; pricePercent?: number; priceRange?: string }[]
// ): number | [number, number] {
// const base = option.price ?? 0
// let total = base
// let hasRange = false
// let min = base
// let max = base
// for (const ext of extras) {
// if (ext.price !== undefined) {
// total += ext.price
// min += ext.price
// max += ext.price
// } else if (ext.pricePercent !== undefined) {
// const delta = base * (ext.pricePercent / 100)
// total += delta
// min += delta
// max += delta
// } else if (ext.priceRange) {
// const [rMin, rMax] = ext.priceRange.split("").map(Number)
// hasRange = true
// min += rMin
// max += rMax
// }
// }
// return hasRange ? [min, max] : total
// }
type PriceSource = {
price?: number | null
pricePercent?: number | null
@ -46,4 +13,36 @@ export function calculatePrice(source: PriceSource, base: number): number {
return isNaN(max) ? 0 : max
}
return 0
}
export function calculatePriceRange(
baseSource: PriceSource | undefined,
extras: PriceSource[]
): [number, number] {
if (!baseSource) return [0, 0]
const base = calculatePrice(baseSource, 0)
let minExtra = 0
let maxExtra = 0
for (const extra of extras) {
if (extra.price != null) {
minExtra += extra.price
maxExtra += extra.price
} else if (extra.pricePercent != null) {
const val = base * (extra.pricePercent / 100)
minExtra += val
maxExtra += val
} else if (extra.priceRange) {
const [minStr, maxStr] = extra.priceRange.split("")
const min = Number(minStr)
const max = Number(maxStr)
if (!isNaN(min)) minExtra += min
if (!isNaN(max)) maxExtra += max
}
}
return [base + minExtra, base + maxExtra]
}