Doing stuff
This commit is contained in:
@ -1,25 +1,32 @@
|
||||
// components/commissions/CommissionCard.tsx
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { CommissionCardProps } from "@/types/commissions"
|
||||
import Image from "next/image"
|
||||
import { useState } from "react"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible"
|
||||
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"
|
||||
// import { useState } from "react"
|
||||
import { Badge } from "../ui/badge"
|
||||
|
||||
export function CommissionCard({ type, options, extras, examples }: CommissionCardProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
type CommissionTypeWithItems = CommissionType & {
|
||||
options: (CommissionTypeOption & {
|
||||
option: CommissionOption | null
|
||||
})[]
|
||||
extras: (CommissionTypeExtra & {
|
||||
extra: CommissionExtra | null
|
||||
})[]
|
||||
}
|
||||
|
||||
export function CommissionCard({ commission }: { commission: CommissionTypeWithItems }) {
|
||||
// const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Card className="flex flex-col flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-bold">{type.name}</CardTitle>
|
||||
<p className="text-muted-foreground text-sm">{type.description}</p>
|
||||
<CardTitle className="text-xl font-bold">{commission.name}</CardTitle>
|
||||
<p className="text-muted-foreground text-sm">{commission.description}</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
{examples && examples.length > 0 && (
|
||||
{/* {examples && examples.length > 0 && (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger className="text-sm underline text-muted-foreground">
|
||||
{open ? "Hide Examples" : "See Examples"}
|
||||
@ -41,13 +48,20 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
)} */}
|
||||
<div>
|
||||
<h4 className="font-semibold">Options</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{options.map((opt) => (
|
||||
<li key={opt.key}>
|
||||
{opt.name}: {opt.price}€
|
||||
{commission.options.map((option) => (
|
||||
<li key={option.id}>
|
||||
{option.option?.name}:{" "}
|
||||
{option.price
|
||||
? `${option.price}€`
|
||||
: option.pricePercent
|
||||
? `+${option.pricePercent}%`
|
||||
: option.priceRange
|
||||
? `${option.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -56,28 +70,28 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
|
||||
<div>
|
||||
<h4 className="font-semibold">Extras</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
{extras.map((extra) => (
|
||||
<li key={extra.key}>
|
||||
{extra.name}:{" "}
|
||||
{extra.price !== undefined
|
||||
{commission.extras.map((extra) => (
|
||||
<li key={extra.id}>
|
||||
{extra.extra?.name}:{" "}
|
||||
{extra.price
|
||||
? `${extra.price}€`
|
||||
: extra.pricePercent
|
||||
? `+${extra.pricePercent}%`
|
||||
: extra.priceRange
|
||||
? `${extra.priceRange.replace("#", "–")}€`
|
||||
? `${extra.priceRange}€`
|
||||
: "Included"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-wrap gap-2">
|
||||
{extras.map((extra) => (
|
||||
<Badge variant="outline" key={extra.key}>
|
||||
{extra.name}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{commission.extras.map((extra) => (
|
||||
<Badge variant="outline" key={extra.id}>
|
||||
{extra.extra?.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
// src/app/(frontend)/commissions/CommissionForm.tsx
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { CommissionFormData, commissionFormSchema } from "@/schemas/commission"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
// import { CommissionFormData, commissionFormSchema } from "./schema"
|
||||
|
||||
export function CommissionForm() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<CommissionFormData>({
|
||||
resolver: zodResolver(commissionFormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: CommissionFormData) {
|
||||
console.log("Order submitted", data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 max-w-md">
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" {...register("name")} />
|
||||
{errors.name && <p className="text-sm text-red-500">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" {...register("email")} />
|
||||
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="type">Commission Type</Label>
|
||||
<Input id="type" {...register("type")} />
|
||||
{errors.type && <p className="text-sm text-red-500">{errors.type.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="message">Description / Message</Label>
|
||||
<Textarea id="message" {...register("message")} rows={5} />
|
||||
{errors.message && <p className="text-sm text-red-500">{errors.message.message}</p>}
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit Request</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
238
src/components/commissions/CommissionOrderForm.tsx
Normal file
238
src/components/commissions/CommissionOrderForm.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} 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 { commissionOrderSchema } from "@/schemas/commissionOrder"
|
||||
import { calculatePrice } from "@/utils/calculatePrice"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMemo, useState } from "react"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import * as z from "zod/v4"
|
||||
import { FileDropzone } from "./FileDropzone"
|
||||
|
||||
type CommissionTypeWithRelations = CommissionType & {
|
||||
options: (CommissionTypeOption & { option: CommissionOption })[]
|
||||
extras: (CommissionTypeExtra & { extra: CommissionExtra })[]
|
||||
}
|
||||
|
||||
type Props = {
|
||||
types: CommissionTypeWithRelations[]
|
||||
}
|
||||
|
||||
export function CommissionOrderForm({ types }: Props) {
|
||||
const form = useForm<z.infer<typeof commissionOrderSchema>>({
|
||||
resolver: zodResolver(commissionOrderSchema),
|
||||
defaultValues: {
|
||||
typeId: "",
|
||||
optionId: "",
|
||||
extraIds: [],
|
||||
customerName: "",
|
||||
customerEmail: "",
|
||||
message: "",
|
||||
},
|
||||
})
|
||||
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const typeId = useWatch({ control: form.control, name: "typeId" })
|
||||
const optionId = useWatch({ control: form.control, name: "optionId" })
|
||||
const extraIds = useWatch({ control: form.control, name: "extraIds" })
|
||||
|
||||
const selectedType = useMemo(
|
||||
() => types.find((t) => t.id === typeId),
|
||||
[types, typeId]
|
||||
)
|
||||
|
||||
const selectedOption = useMemo(
|
||||
() => selectedType?.options.find((o) => o.optionId === optionId),
|
||||
[selectedType, optionId]
|
||||
)
|
||||
|
||||
const selectedExtras = useMemo(
|
||||
() => selectedType?.extras.filter((e) => extraIds?.includes(e.extraId)) ?? [],
|
||||
[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
|
||||
}, [selectedOption, selectedExtras])
|
||||
|
||||
async function onSubmit(values: z.infer<typeof commissionOrderSchema>) {
|
||||
console.log("Submit:", { ...values, files })
|
||||
// TODO: send to server
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="typeId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Choose a commission type</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{types.map((type) => (
|
||||
<Button
|
||||
key={type.id}
|
||||
type="button"
|
||||
variant={field.value === type.id ? "default" : "outline"}
|
||||
onClick={() => field.onChange(type.id)}
|
||||
>
|
||||
{type.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{selectedType && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="optionId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Base Option</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-1">
|
||||
{selectedType.options.map((opt) => (
|
||||
<label key={opt.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
checked={field.value === opt.optionId}
|
||||
value={opt.optionId}
|
||||
onChange={() => field.onChange(opt.optionId)}
|
||||
/>
|
||||
{opt.option.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extraIds"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Extras</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-1">
|
||||
{selectedType?.extras.map((ext) => (
|
||||
<label key={ext.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value?.includes(ext.extraId) ?? false}
|
||||
value={ext.extraId}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked
|
||||
const newSet = new Set(field.value ?? [])
|
||||
if (checked) {
|
||||
newSet.add(ext.extraId)
|
||||
} else {
|
||||
newSet.delete(ext.extraId)
|
||||
}
|
||||
field.onChange(Array.from(newSet))
|
||||
}}
|
||||
/>
|
||||
{ext.extra.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customerName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Your Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Jane Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customerEmail"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="jane@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="message"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Project Details</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Describe what you’d like drawn..."
|
||||
rows={4}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<FormLabel>Reference Images</FormLabel>
|
||||
<FileDropzone onFilesSelected={(f: File[]) => setFiles(f)} />
|
||||
</div>
|
||||
|
||||
<div className="text-xl font-semibold">
|
||||
Estimated Price: €{price.toFixed(2)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={!form.formState.isValid}>
|
||||
Submit Request
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
24
src/components/commissions/FileDropzone.tsx
Normal file
24
src/components/commissions/FileDropzone.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
// components/form/FileDropzone.tsx
|
||||
"use client"
|
||||
import { useCallback } from "react"
|
||||
|
||||
export function FileDropzone({
|
||||
onFilesSelected,
|
||||
}: {
|
||||
onFilesSelected: (files: File[]) => void
|
||||
}) {
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
onFilesSelected(Array.from(e.target.files))
|
||||
}
|
||||
}, [onFilesSelected])
|
||||
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleChange}
|
||||
className="border p-2 rounded-md"
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user