diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e04b9de
--- /dev/null
+++ b/Dockerfile
@@ -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"]
diff --git a/package-lock.json b/package-lock.json
index c0afc92..fc1f135 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 6974fce..f442455 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index aaeee1b..dc6855f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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())
+// }
diff --git a/src/app/commissions/page.tsx b/src/app/commissions/page.tsx
index 3bc15cb..643e335 100644
--- a/src/app/commissions/page.tsx
+++ b/src/app/commissions/page.tsx
@@ -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 (
Commission Pricing
- {commissions.map((data) => (
-
+ {commissions.map((commission) => (
+
))}
Request a Commission
- {/*
*/}
+
);
}
\ No newline at end of file
diff --git a/src/components/commissions/CommissionCard.tsx b/src/components/commissions/CommissionCard.tsx
index dd7bc91..76ab9ff 100644
--- a/src/components/commissions/CommissionCard.tsx
+++ b/src/components/commissions/CommissionCard.tsx
@@ -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 (
- {type.name}
- {type.description}
+ {commission.name}
+ {commission.description}
- {examples && examples.length > 0 && (
+ {/* {examples && examples.length > 0 && (
{open ? "Hide Examples" : "See Examples"}
@@ -41,13 +48,20 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
- )}
+ )} */}
Options
- {options.map((opt) => (
- -
- {opt.name}: {opt.price}€
+ {commission.options.map((option) => (
+
-
+ {option.option?.name}:{" "}
+ {option.price
+ ? `${option.price}€`
+ : option.pricePercent
+ ? `+${option.pricePercent}%`
+ : option.priceRange
+ ? `${option.priceRange}€`
+ : "Included"}
))}
@@ -56,28 +70,28 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
Extras
- {extras.map((extra) => (
- -
- {extra.name}:{" "}
- {extra.price !== undefined
+ {commission.extras.map((extra) => (
+
-
+ {extra.extra?.name}:{" "}
+ {extra.price
? `${extra.price}€`
: extra.pricePercent
? `+${extra.pricePercent}%`
: extra.priceRange
- ? `${extra.priceRange.replace("#", "–")}€`
+ ? `${extra.priceRange}€`
: "Included"}
))}
- {/*
- {extras.map((extra) => (
-
- {extra.name}
+
+ {commission.extras.map((extra) => (
+
+ {extra.extra?.name}
))}
-
*/}
+
diff --git a/src/components/commissions/CommissionForm.tsx b/src/components/commissions/CommissionForm.tsx
deleted file mode 100644
index d9cba39..0000000
--- a/src/components/commissions/CommissionForm.tsx
+++ /dev/null
@@ -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({
- resolver: zodResolver(commissionFormSchema),
- })
-
- function onSubmit(data: CommissionFormData) {
- console.log("Order submitted", data)
- }
-
- return (
-
- )
-}
diff --git a/src/components/commissions/CommissionOrderForm.tsx b/src/components/commissions/CommissionOrderForm.tsx
new file mode 100644
index 0000000..232b8b1
--- /dev/null
+++ b/src/components/commissions/CommissionOrderForm.tsx
@@ -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>({
+ resolver: zodResolver(commissionOrderSchema),
+ defaultValues: {
+ typeId: "",
+ optionId: "",
+ extraIds: [],
+ customerName: "",
+ customerEmail: "",
+ message: "",
+ },
+ })
+
+ const [files, setFiles] = useState([])
+
+ 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) {
+ console.log("Submit:", { ...values, files })
+ // TODO: send to server
+ }
+
+ return (
+
+
+ )
+}
diff --git a/src/components/commissions/FileDropzone.tsx b/src/components/commissions/FileDropzone.tsx
new file mode 100644
index 0000000..257731d
--- /dev/null
+++ b/src/components/commissions/FileDropzone.tsx
@@ -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) => {
+ if (e.target.files) {
+ onFilesSelected(Array.from(e.target.files))
+ }
+ }, [onFilesSelected])
+
+ return (
+
+ )
+}
diff --git a/src/data/mockCommissions.ts b/src/data/mockCommissions.ts
index 06ccc48..bc8f637 100644
--- a/src/data/mockCommissions.ts
+++ b/src/data/mockCommissions.ts
@@ -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'
+// ]
+// }
+// ]
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..7db18c7
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -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
\ No newline at end of file
diff --git a/src/schemas/commission.ts b/src/schemas/commission.ts
index a437eb0..52c8307 100644
--- a/src/schemas/commission.ts
+++ b/src/schemas/commission.ts
@@ -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
\ No newline at end of file
+// export type commissionOrderSchema = z.infer
\ No newline at end of file
diff --git a/src/schemas/commissionOrder.ts b/src/schemas/commissionOrder.ts
new file mode 100644
index 0000000..887e6fb
--- /dev/null
+++ b/src/schemas/commissionOrder.ts
@@ -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"),
+})
diff --git a/src/types/commissions.ts b/src/types/commissions.ts
index 3c07e83..6d8b3cc 100644
--- a/src/types/commissions.ts
+++ b/src/types/commissions.ts
@@ -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[]
-}
\ No newline at end of file
+// 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[]
+// }
\ No newline at end of file
diff --git a/src/utils/calculatePrice.ts b/src/utils/calculatePrice.ts
new file mode 100644
index 0000000..7a27aa3
--- /dev/null
+++ b/src/utils/calculatePrice.ts
@@ -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
+}
\ No newline at end of file