Doing stuff
This commit is contained in:
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
# Base image
|
||||
FROM node:20
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the code
|
||||
COPY . .
|
||||
RUN npx prisma generate
|
||||
|
||||
# Expose the dev port
|
||||
EXPOSE 3000
|
||||
|
||||
# Run dev server
|
||||
CMD ["npm", "run", "dev"]
|
41
package-lock.json
generated
41
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
@ -1041,11 +1042,33 @@
|
||||
"url": "https://opencollective.com/pkgr"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.1.tgz",
|
||||
"integrity": "sha512-5CLFh8QP6KxRm83pJ84jaVCeSVPQr8k0L2SEtOJHwdkS57/VQDcI/wQpGmdyOZi+D9gdNabdo8tj1Uk+w+upsQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*",
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.1.tgz",
|
||||
"integrity": "sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jiti": "2.4.2"
|
||||
@ -1055,14 +1078,14 @@
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.1.tgz",
|
||||
"integrity": "sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.1.tgz",
|
||||
"integrity": "sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -1076,14 +1099,14 @@
|
||||
"version": "6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9.tgz",
|
||||
"integrity": "sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.1.tgz",
|
||||
"integrity": "sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.11.1",
|
||||
@ -1095,7 +1118,7 @@
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.1.tgz",
|
||||
"integrity": "sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.11.1"
|
||||
@ -5124,7 +5147,7 @@
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@ -6107,7 +6130,7 @@
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.1.tgz",
|
||||
"integrity": "sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -7168,7 +7191,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@prisma/client": "^6.11.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
|
@ -13,3 +13,162 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model CommissionType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String
|
||||
|
||||
description String?
|
||||
|
||||
options CommissionTypeOption[]
|
||||
extras CommissionTypeExtra[]
|
||||
}
|
||||
|
||||
model CommissionOption {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
description String?
|
||||
|
||||
types CommissionTypeOption[]
|
||||
}
|
||||
|
||||
model CommissionExtra {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
description String?
|
||||
|
||||
types CommissionTypeExtra[]
|
||||
}
|
||||
|
||||
model CommissionTypeOption {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
typeId String
|
||||
optionId String
|
||||
|
||||
priceRange String?
|
||||
pricePercent Float?
|
||||
price Float?
|
||||
|
||||
type CommissionType @relation(fields: [typeId], references: [id])
|
||||
option CommissionOption @relation(fields: [optionId], references: [id])
|
||||
|
||||
@@unique([typeId, optionId])
|
||||
}
|
||||
|
||||
model CommissionTypeExtra {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
typeId String
|
||||
extraId String
|
||||
|
||||
priceRange String?
|
||||
pricePercent Float?
|
||||
price Float?
|
||||
|
||||
type CommissionType @relation(fields: [typeId], references: [id])
|
||||
extra CommissionExtra @relation(fields: [extraId], references: [id])
|
||||
|
||||
@@unique([typeId, extraId])
|
||||
}
|
||||
|
||||
// model User {
|
||||
// id String @id @default(cuid())
|
||||
// email String @unique
|
||||
// name String?
|
||||
// role Role @default(ADMIN)
|
||||
// createdAt DateTime @default(now())
|
||||
// }
|
||||
|
||||
// enum Role {
|
||||
// ADMIN
|
||||
// ARTIST
|
||||
// }
|
||||
|
||||
// model CommissionType {
|
||||
// id String @id @default(cuid())
|
||||
// title String
|
||||
// description String?
|
||||
// basePrice Float
|
||||
// deliveryEst String? // e.g. "2 weeks"
|
||||
// tags String[] // e.g. shaded, sketch, full-body
|
||||
// active Boolean @default(true)
|
||||
// createdAt DateTime @default(now())
|
||||
// CommissionRequest CommissionRequest[]
|
||||
// }
|
||||
|
||||
// model CommissionRequest {
|
||||
// id String @id @default(cuid())
|
||||
// name String
|
||||
// email String
|
||||
// message String
|
||||
// typeId String
|
||||
// status RequestStatus @default(PENDING)
|
||||
// createdAt DateTime @default(now())
|
||||
|
||||
// type CommissionType @relation(fields: [typeId], references: [id])
|
||||
// }
|
||||
|
||||
// enum RequestStatus {
|
||||
// PENDING
|
||||
// ACCEPTED
|
||||
// IN_PROGRESS
|
||||
// DONE
|
||||
// REJECTED
|
||||
// }
|
||||
|
||||
// model Artwork {
|
||||
// id String @id @default(cuid())
|
||||
// title String
|
||||
// imageUrl String
|
||||
// description String?
|
||||
// tags String[]
|
||||
// formats String[]
|
||||
// isPublic Boolean @default(true)
|
||||
// groupId String?
|
||||
// createdAt DateTime @default(now())
|
||||
|
||||
// group PresentationGroup? @relation(fields: [groupId], references: [id])
|
||||
// }
|
||||
|
||||
// model PresentationGroup {
|
||||
// id String @id @default(cuid())
|
||||
// name String
|
||||
// description String?
|
||||
// createdAt DateTime @default(now())
|
||||
// Artwork Artwork[]
|
||||
// }
|
||||
|
||||
// model Preferences {
|
||||
// id String @id @default(cuid())
|
||||
// commissionOpen Boolean @default(true)
|
||||
// defaultDelivery String? // e.g. "7 days"
|
||||
// autoReplyMessage String?
|
||||
// notifyByEmail Boolean @default(true)
|
||||
// }
|
||||
|
||||
// model TOS {
|
||||
// id String @id @default(cuid())
|
||||
// content String // Markdown or rich text
|
||||
// createdAt DateTime @default(now())
|
||||
// }
|
||||
|
@ -1,20 +1,27 @@
|
||||
import { CommissionCard } from "@/components/commissions/CommissionCard";
|
||||
import { generateMockCommissions } from "@/utils/generateMockCommissions";
|
||||
import { CommissionOrderForm } from "@/components/commissions/CommissionOrderForm";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default function CommissionsPage() {
|
||||
const commissions = generateMockCommissions()
|
||||
export default async function CommissionsPage() {
|
||||
const commissions = await prisma.commissionType.findMany({
|
||||
include: {
|
||||
options: { include: { option: true }, orderBy: { sortIndex: "asc" } },
|
||||
extras: { include: { extra: true }, orderBy: { sortIndex: "asc" } },
|
||||
},
|
||||
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container py-10 space-y-10">
|
||||
<h1 className="text-3xl font-bold">Commission Pricing</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
|
||||
{commissions.map((data) => (
|
||||
<CommissionCard key={data.type.key} {...data} />
|
||||
{commissions.map((commission) => (
|
||||
<CommissionCard key={commission.id} commission={commission} />
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<h2 className="text-2xl font-semibold">Request a Commission</h2>
|
||||
{/* <CommissionForm /> */}
|
||||
<CommissionOrderForm types={commissions} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,311 +1,311 @@
|
||||
|
||||
export const commissionTypes = {
|
||||
sketch: {
|
||||
name: 'Sketch',
|
||||
description: 'A quick sketch of a character',
|
||||
},
|
||||
flat: {
|
||||
name: 'Flat Color / Lineart',
|
||||
description: 'Dunno how to describe this',
|
||||
},
|
||||
shaded: {
|
||||
name: 'Shaded',
|
||||
description: 'Dunno how to describe this',
|
||||
},
|
||||
refsheet: {
|
||||
name: 'Reference Sheet',
|
||||
description: 'A reference sheet with multiple poses',
|
||||
}
|
||||
}
|
||||
// export const commissionTypes = {
|
||||
// sketch: {
|
||||
// name: 'Sketch',
|
||||
// description: 'A quick sketch of a character',
|
||||
// },
|
||||
// flat: {
|
||||
// name: 'Flat Color / Lineart',
|
||||
// description: 'Dunno how to describe this',
|
||||
// },
|
||||
// shaded: {
|
||||
// name: 'Shaded',
|
||||
// description: 'Dunno how to describe this',
|
||||
// },
|
||||
// refsheet: {
|
||||
// name: 'Reference Sheet',
|
||||
// description: 'A reference sheet with multiple poses',
|
||||
// }
|
||||
// }
|
||||
|
||||
export const commissionOptions = {
|
||||
headshot: {
|
||||
name: 'Headshot',
|
||||
basePrice: 10
|
||||
},
|
||||
halfbody: {
|
||||
name: 'Halfbody',
|
||||
basePrice: 15
|
||||
},
|
||||
fullbody: {
|
||||
name: 'Fullbody',
|
||||
basePrice: 20
|
||||
}
|
||||
}
|
||||
// export const commissionOptions = {
|
||||
// headshot: {
|
||||
// name: 'Headshot',
|
||||
// basePrice: 10
|
||||
// },
|
||||
// halfbody: {
|
||||
// name: 'Halfbody',
|
||||
// basePrice: 15
|
||||
// },
|
||||
// fullbody: {
|
||||
// name: 'Fullbody',
|
||||
// basePrice: 20
|
||||
// }
|
||||
// }
|
||||
|
||||
export const commissionExtras = {
|
||||
extraChar: {
|
||||
name: 'Extra Character',
|
||||
description: 'Add an extra character to the commission',
|
||||
pricePercent: 75,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
basicBackground: {
|
||||
name: 'Basic Background',
|
||||
description: 'Add a basic background to the commission',
|
||||
pricePercent: 0,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
abstractBackground: {
|
||||
name: 'Abstract Background',
|
||||
description: 'Add an abstract background to the commission',
|
||||
pricePercent: 0,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
gradientBackground: {
|
||||
name: 'Gradient Background',
|
||||
description: 'Add an gradient background to the commission',
|
||||
pricePercent: 0,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
customBackground: {
|
||||
name: 'Custom Background',
|
||||
description: 'Add a custom background to the commission',
|
||||
pricePercent: 0,
|
||||
price: 0,
|
||||
priceRange: "10#50"
|
||||
},
|
||||
nsfw: {
|
||||
name: 'NSFW',
|
||||
description: 'Add NSFW content to the commission',
|
||||
pricePercent: 15,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
normalComplexity: {
|
||||
name: 'Light Complexity',
|
||||
description: 'Increase complexity of the commission',
|
||||
pricePercent: 15,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
highComplexity: {
|
||||
name: 'High Complexity',
|
||||
description: 'Highly increase complexity of the commission',
|
||||
pricePercent: 30,
|
||||
price: 0,
|
||||
priceRange: ""
|
||||
},
|
||||
details: {
|
||||
name: 'Details',
|
||||
description: 'Add details to the commission (tails, eyes, ...)',
|
||||
pricePercent: 0,
|
||||
price: 0,
|
||||
priceRange: "3#5"
|
||||
},
|
||||
shading: {
|
||||
name: 'Shading',
|
||||
description: 'Add shading to the commission',
|
||||
pricePercent: 0,
|
||||
price: 20,
|
||||
priceRange: ""
|
||||
}
|
||||
}
|
||||
// export const commissionExtras = {
|
||||
// extraChar: {
|
||||
// name: 'Extra Character',
|
||||
// description: 'Add an extra character to the commission',
|
||||
// pricePercent: 75,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// basicBackground: {
|
||||
// name: 'Basic Background',
|
||||
// description: 'Add a basic background to the commission',
|
||||
// pricePercent: 0,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// abstractBackground: {
|
||||
// name: 'Abstract Background',
|
||||
// description: 'Add an abstract background to the commission',
|
||||
// pricePercent: 0,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// gradientBackground: {
|
||||
// name: 'Gradient Background',
|
||||
// description: 'Add an gradient background to the commission',
|
||||
// pricePercent: 0,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// customBackground: {
|
||||
// name: 'Custom Background',
|
||||
// description: 'Add a custom background to the commission',
|
||||
// pricePercent: 0,
|
||||
// price: 0,
|
||||
// priceRange: "10#50"
|
||||
// },
|
||||
// nsfw: {
|
||||
// name: 'NSFW',
|
||||
// description: 'Add NSFW content to the commission',
|
||||
// pricePercent: 15,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// normalComplexity: {
|
||||
// name: 'Light Complexity',
|
||||
// description: 'Increase complexity of the commission',
|
||||
// pricePercent: 15,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// highComplexity: {
|
||||
// name: 'High Complexity',
|
||||
// description: 'Highly increase complexity of the commission',
|
||||
// pricePercent: 30,
|
||||
// price: 0,
|
||||
// priceRange: ""
|
||||
// },
|
||||
// details: {
|
||||
// name: 'Details',
|
||||
// description: 'Add details to the commission (tails, eyes, ...)',
|
||||
// pricePercent: 0,
|
||||
// price: 0,
|
||||
// priceRange: "3#5"
|
||||
// },
|
||||
// shading: {
|
||||
// name: 'Shading',
|
||||
// description: 'Add shading to the commission',
|
||||
// pricePercent: 0,
|
||||
// price: 20,
|
||||
// priceRange: ""
|
||||
// }
|
||||
// }
|
||||
|
||||
export const rawCommissions = [
|
||||
{
|
||||
type: commissionTypes.sketch,
|
||||
options: [
|
||||
{
|
||||
type: commissionOptions.headshot,
|
||||
price: commissionOptions.headshot.basePrice
|
||||
},
|
||||
{
|
||||
type: commissionOptions.halfbody,
|
||||
price: commissionOptions.halfbody.basePrice
|
||||
},
|
||||
{
|
||||
type: commissionOptions.fullbody,
|
||||
price: commissionOptions.fullbody.basePrice
|
||||
}
|
||||
],
|
||||
extras: [
|
||||
{
|
||||
type: commissionExtras.extraChar,
|
||||
pricePercent: commissionExtras.extraChar.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.basicBackground,
|
||||
price: commissionExtras.basicBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.abstractBackground,
|
||||
price: commissionExtras.abstractBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.gradientBackground,
|
||||
price: commissionExtras.gradientBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.customBackground,
|
||||
priceRange: commissionExtras.customBackground.priceRange
|
||||
},
|
||||
{
|
||||
type: commissionExtras.nsfw,
|
||||
pricePercent: commissionExtras.nsfw.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.normalComplexity,
|
||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.highComplexity,
|
||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
'/examples/sketch1.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: commissionTypes.flat,
|
||||
options: [
|
||||
{
|
||||
type: commissionOptions.headshot,
|
||||
price: 18
|
||||
},
|
||||
{
|
||||
type: commissionOptions.halfbody,
|
||||
price: 22
|
||||
},
|
||||
{
|
||||
type: commissionOptions.fullbody,
|
||||
price: 28
|
||||
}
|
||||
],
|
||||
extras: [
|
||||
{
|
||||
type: commissionExtras.extraChar,
|
||||
pricePercent: commissionExtras.extraChar.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.basicBackground,
|
||||
price: commissionExtras.basicBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.abstractBackground,
|
||||
price: commissionExtras.abstractBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.gradientBackground,
|
||||
price: commissionExtras.gradientBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.customBackground,
|
||||
priceRange: commissionExtras.customBackground.priceRange
|
||||
},
|
||||
{
|
||||
type: commissionExtras.nsfw,
|
||||
pricePercent: commissionExtras.nsfw.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.normalComplexity,
|
||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.highComplexity,
|
||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
}
|
||||
],
|
||||
examples: [
|
||||
'/examples/flat1.png',
|
||||
'/examples/flat2.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: commissionTypes.shaded,
|
||||
options: [
|
||||
{
|
||||
type: commissionOptions.headshot,
|
||||
price: 25
|
||||
},
|
||||
{
|
||||
type: commissionOptions.halfbody,
|
||||
price: 40
|
||||
},
|
||||
{
|
||||
type: commissionOptions.fullbody,
|
||||
price: 55
|
||||
}
|
||||
],
|
||||
extras: [
|
||||
{
|
||||
type: commissionExtras.extraChar,
|
||||
pricePercent: commissionExtras.extraChar.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.basicBackground,
|
||||
price: commissionExtras.basicBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.abstractBackground,
|
||||
price: commissionExtras.abstractBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.gradientBackground,
|
||||
price: commissionExtras.gradientBackground.price
|
||||
},
|
||||
{
|
||||
type: commissionExtras.customBackground,
|
||||
priceRange: "20#100"
|
||||
},
|
||||
{
|
||||
type: commissionExtras.nsfw,
|
||||
pricePercent: commissionExtras.nsfw.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.normalComplexity,
|
||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.highComplexity,
|
||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
}
|
||||
],
|
||||
examples: [
|
||||
'/examples/shaded1.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
type: commissionTypes.refsheet,
|
||||
options: [
|
||||
{
|
||||
type: commissionOptions.fullbody,
|
||||
price: 25
|
||||
}
|
||||
],
|
||||
extras: [
|
||||
{
|
||||
type: commissionExtras.customBackground,
|
||||
priceRange: "20#100"
|
||||
},
|
||||
{
|
||||
type: commissionExtras.nsfw,
|
||||
pricePercent: commissionExtras.nsfw.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.normalComplexity,
|
||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.highComplexity,
|
||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
},
|
||||
{
|
||||
type: commissionExtras.details,
|
||||
priceRange: commissionExtras.details.priceRange
|
||||
},
|
||||
{
|
||||
type: commissionExtras.shading,
|
||||
price: commissionExtras.shading.price
|
||||
}
|
||||
],
|
||||
examples: [
|
||||
'/examples/refsheet1.png'
|
||||
]
|
||||
}
|
||||
]
|
||||
// export const rawCommissions = [
|
||||
// {
|
||||
// type: commissionTypes.sketch,
|
||||
// options: [
|
||||
// {
|
||||
// type: commissionOptions.headshot,
|
||||
// price: commissionOptions.headshot.basePrice
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.halfbody,
|
||||
// price: commissionOptions.halfbody.basePrice
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.fullbody,
|
||||
// price: commissionOptions.fullbody.basePrice
|
||||
// }
|
||||
// ],
|
||||
// extras: [
|
||||
// {
|
||||
// type: commissionExtras.extraChar,
|
||||
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.basicBackground,
|
||||
// price: commissionExtras.basicBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.abstractBackground,
|
||||
// price: commissionExtras.abstractBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.gradientBackground,
|
||||
// price: commissionExtras.gradientBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.customBackground,
|
||||
// priceRange: commissionExtras.customBackground.priceRange
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.nsfw,
|
||||
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.normalComplexity,
|
||||
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.highComplexity,
|
||||
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
// },
|
||||
// ],
|
||||
// examples: [
|
||||
// '/examples/sketch1.png'
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// type: commissionTypes.flat,
|
||||
// options: [
|
||||
// {
|
||||
// type: commissionOptions.headshot,
|
||||
// price: 18
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.halfbody,
|
||||
// price: 22
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.fullbody,
|
||||
// price: 28
|
||||
// }
|
||||
// ],
|
||||
// extras: [
|
||||
// {
|
||||
// type: commissionExtras.extraChar,
|
||||
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.basicBackground,
|
||||
// price: commissionExtras.basicBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.abstractBackground,
|
||||
// price: commissionExtras.abstractBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.gradientBackground,
|
||||
// price: commissionExtras.gradientBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.customBackground,
|
||||
// priceRange: commissionExtras.customBackground.priceRange
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.nsfw,
|
||||
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.normalComplexity,
|
||||
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.highComplexity,
|
||||
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
// }
|
||||
// ],
|
||||
// examples: [
|
||||
// '/examples/flat1.png',
|
||||
// '/examples/flat2.png'
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// type: commissionTypes.shaded,
|
||||
// options: [
|
||||
// {
|
||||
// type: commissionOptions.headshot,
|
||||
// price: 25
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.halfbody,
|
||||
// price: 40
|
||||
// },
|
||||
// {
|
||||
// type: commissionOptions.fullbody,
|
||||
// price: 55
|
||||
// }
|
||||
// ],
|
||||
// extras: [
|
||||
// {
|
||||
// type: commissionExtras.extraChar,
|
||||
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.basicBackground,
|
||||
// price: commissionExtras.basicBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.abstractBackground,
|
||||
// price: commissionExtras.abstractBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.gradientBackground,
|
||||
// price: commissionExtras.gradientBackground.price
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.customBackground,
|
||||
// priceRange: "20#100"
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.nsfw,
|
||||
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.normalComplexity,
|
||||
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.highComplexity,
|
||||
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
// }
|
||||
// ],
|
||||
// examples: [
|
||||
// '/examples/shaded1.png'
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// type: commissionTypes.refsheet,
|
||||
// options: [
|
||||
// {
|
||||
// type: commissionOptions.fullbody,
|
||||
// price: 25
|
||||
// }
|
||||
// ],
|
||||
// extras: [
|
||||
// {
|
||||
// type: commissionExtras.customBackground,
|
||||
// priceRange: "20#100"
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.nsfw,
|
||||
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.normalComplexity,
|
||||
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.highComplexity,
|
||||
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.details,
|
||||
// priceRange: commissionExtras.details.priceRange
|
||||
// },
|
||||
// {
|
||||
// type: commissionExtras.shading,
|
||||
// price: commissionExtras.shading.price
|
||||
// }
|
||||
// ],
|
||||
// examples: [
|
||||
// '/examples/refsheet1.png'
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
|
14
src/lib/prisma.ts
Normal file
14
src/lib/prisma.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// import { PrismaClient } from '@/types/prisma'
|
||||
// import { withAccelerate } from '@prisma/extension-accelerate'
|
||||
|
||||
import { PrismaClient } from "@/generated/prisma"
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient
|
||||
}
|
||||
|
||||
const prisma = globalForPrisma.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
|
||||
|
||||
export default prisma
|
@ -1,14 +1,14 @@
|
||||
import { z } from "zod"
|
||||
// import { z } from "zod"
|
||||
|
||||
export const commissionOrderSchema = z.object({
|
||||
type: z.enum(["sketch", "flat", "shaded", "refsheet"]),
|
||||
body: z.enum(["headshot", "portrait", "waistup", "fullbody"]).optional(),
|
||||
extras: z.array(z.string()).optional(),
|
||||
customExtras: z.string().optional(),
|
||||
description: z.string().min(10),
|
||||
contact: z.string().email("Please enter a valid email"),
|
||||
referenceFiles: z.array(z.any()).optional(), // for now just accept any File[]
|
||||
tosAccepted: z.literal(true), // must be checked
|
||||
})
|
||||
// export const commissionOrderSchema = z.object({
|
||||
// type: z.enum(["sketch", "flat", "shaded", "refsheet"]),
|
||||
// body: z.enum(["headshot", "portrait", "waistup", "fullbody"]).optional(),
|
||||
// extras: z.array(z.string()).optional(),
|
||||
// customExtras: z.string().optional(),
|
||||
// description: z.string().min(10),
|
||||
// contact: z.string().email("Please enter a valid email"),
|
||||
// referenceFiles: z.array(z.any()).optional(), // for now just accept any File[]
|
||||
// tosAccepted: z.literal(true), // must be checked
|
||||
// })
|
||||
|
||||
export type commissionOrderSchema = z.infer<typeof commissionOrderSchema>
|
||||
// export type commissionOrderSchema = z.infer<typeof commissionOrderSchema>
|
10
src/schemas/commissionOrder.ts
Normal file
10
src/schemas/commissionOrder.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as z from "zod/v4"
|
||||
|
||||
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(),
|
||||
customerName: z.string().min(2, "Enter your name"),
|
||||
customerEmail: z.string().email("Invalid email"),
|
||||
message: z.string().min(5, "Please describe what you want"),
|
||||
})
|
@ -1,59 +1,59 @@
|
||||
// types/commissions.ts
|
||||
// // types/commissions.ts
|
||||
|
||||
export interface CommissionType {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
// export interface CommissionType {
|
||||
// name: string
|
||||
// description: string
|
||||
// }
|
||||
|
||||
export interface CommissionOptionType {
|
||||
name: string
|
||||
basePrice: number
|
||||
}
|
||||
// export interface CommissionOptionType {
|
||||
// name: string
|
||||
// basePrice: number
|
||||
// }
|
||||
|
||||
export interface CommissionOption {
|
||||
type: CommissionOptionType
|
||||
price: number
|
||||
}
|
||||
// export interface CommissionOption {
|
||||
// type: CommissionOptionType
|
||||
// price: number
|
||||
// }
|
||||
|
||||
export interface CommissionExtraType {
|
||||
name: string
|
||||
description: string
|
||||
pricePercent?: number
|
||||
price?: number
|
||||
priceRange?: string
|
||||
}
|
||||
// export interface CommissionExtraType {
|
||||
// name: string
|
||||
// description: string
|
||||
// pricePercent?: number
|
||||
// price?: number
|
||||
// priceRange?: string
|
||||
// }
|
||||
|
||||
export interface CommissionExtra {
|
||||
type: CommissionExtraType
|
||||
price?: number
|
||||
pricePercent?: number
|
||||
priceRange?: string
|
||||
}
|
||||
// export interface CommissionExtra {
|
||||
// type: CommissionExtraType
|
||||
// price?: number
|
||||
// pricePercent?: number
|
||||
// priceRange?: string
|
||||
// }
|
||||
|
||||
export interface Commission {
|
||||
type: CommissionType
|
||||
options: CommissionOption[]
|
||||
extras: CommissionExtra[]
|
||||
}
|
||||
// export interface Commission {
|
||||
// type: CommissionType
|
||||
// options: CommissionOption[]
|
||||
// extras: CommissionExtra[]
|
||||
// }
|
||||
|
||||
export interface CommissionCardProps {
|
||||
type: {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
options: {
|
||||
key: string
|
||||
name: string
|
||||
price: number
|
||||
}[]
|
||||
extras: {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
price?: number
|
||||
pricePercent?: number
|
||||
priceRange?: string
|
||||
}[],
|
||||
examples?: string[]
|
||||
}
|
||||
// export interface CommissionCardProps {
|
||||
// type: {
|
||||
// key: string
|
||||
// name: string
|
||||
// description: string
|
||||
// }
|
||||
// options: {
|
||||
// key: string
|
||||
// name: string
|
||||
// price: number
|
||||
// }[]
|
||||
// extras: {
|
||||
// key: string
|
||||
// name: string
|
||||
// description: string
|
||||
// price?: number
|
||||
// pricePercent?: number
|
||||
// priceRange?: string
|
||||
// }[],
|
||||
// examples?: string[]
|
||||
// }
|
49
src/utils/calculatePrice.ts
Normal file
49
src/utils/calculatePrice.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// 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
|
||||
priceRange?: string | null
|
||||
}
|
||||
|
||||
export function calculatePrice(source: PriceSource, base: number): number {
|
||||
if (source.price != null) return source.price
|
||||
if (source.pricePercent != null) return base * (source.pricePercent / 100)
|
||||
if (source.priceRange) {
|
||||
const parts = source.priceRange.split("–").map(Number)
|
||||
const max = Math.max(...parts)
|
||||
return isNaN(max) ? 0 : max
|
||||
}
|
||||
return 0
|
||||
}
|
Reference in New Issue
Block a user