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",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@prisma/client": "^6.11.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
@ -1041,11 +1042,33 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"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": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.1.tgz",
|
||||||
"integrity": "sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==",
|
"integrity": "sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jiti": "2.4.2"
|
"jiti": "2.4.2"
|
||||||
@ -1055,14 +1078,14 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.1.tgz",
|
||||||
"integrity": "sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==",
|
"integrity": "sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.1.tgz",
|
||||||
"integrity": "sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==",
|
"integrity": "sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1076,14 +1099,14 @@
|
|||||||
"version": "6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9",
|
"version": "6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9.tgz",
|
||||||
"integrity": "sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==",
|
"integrity": "sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.1.tgz",
|
||||||
"integrity": "sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==",
|
"integrity": "sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.11.1",
|
"@prisma/debug": "6.11.1",
|
||||||
@ -1095,7 +1118,7 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.1.tgz",
|
||||||
"integrity": "sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==",
|
"integrity": "sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.11.1"
|
"@prisma/debug": "6.11.1"
|
||||||
@ -5124,7 +5147,7 @@
|
|||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@ -6107,7 +6130,7 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.1.tgz",
|
||||||
"integrity": "sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==",
|
"integrity": "sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -7168,7 +7191,7 @@
|
|||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
|
"@prisma/client": "^6.11.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
@ -13,3 +13,162 @@ datasource db {
|
|||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
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 { 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() {
|
export default async function CommissionsPage() {
|
||||||
const commissions = generateMockCommissions()
|
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 (
|
return (
|
||||||
<div className="container py-10 space-y-10">
|
<div className="container py-10 space-y-10">
|
||||||
<h1 className="text-3xl font-bold">Commission Pricing</h1>
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
|
||||||
{commissions.map((data) => (
|
{commissions.map((commission) => (
|
||||||
<CommissionCard key={data.type.key} {...data} />
|
<CommissionCard key={commission.id} commission={commission} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 className="text-2xl font-semibold">Request a Commission</h2>
|
<h2 className="text-2xl font-semibold">Request a Commission</h2>
|
||||||
{/* <CommissionForm /> */}
|
<CommissionOrderForm types={commissions} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,25 +1,32 @@
|
|||||||
// components/commissions/CommissionCard.tsx
|
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { CommissionCardProps } from "@/types/commissions"
|
import { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma"
|
||||||
import Image from "next/image"
|
// import { useState } from "react"
|
||||||
import { useState } from "react"
|
import { Badge } from "../ui/badge"
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible"
|
|
||||||
|
|
||||||
export function CommissionCard({ type, options, extras, examples }: CommissionCardProps) {
|
type CommissionTypeWithItems = CommissionType & {
|
||||||
const [open, setOpen] = useState(false)
|
options: (CommissionTypeOption & {
|
||||||
|
option: CommissionOption | null
|
||||||
|
})[]
|
||||||
|
extras: (CommissionTypeExtra & {
|
||||||
|
extra: CommissionExtra | null
|
||||||
|
})[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommissionCard({ commission }: { commission: CommissionTypeWithItems }) {
|
||||||
|
// const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<Card className="flex flex-col flex-1">
|
<Card className="flex flex-col flex-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl font-bold">{type.name}</CardTitle>
|
<CardTitle className="text-xl font-bold">{commission.name}</CardTitle>
|
||||||
<p className="text-muted-foreground text-sm">{type.description}</p>
|
<p className="text-muted-foreground text-sm">{commission.description}</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col justify-start gap-4">
|
<CardContent className="flex flex-col justify-start gap-4">
|
||||||
{examples && examples.length > 0 && (
|
{/* {examples && examples.length > 0 && (
|
||||||
<Collapsible open={open} onOpenChange={setOpen}>
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
<CollapsibleTrigger className="text-sm underline text-muted-foreground">
|
<CollapsibleTrigger className="text-sm underline text-muted-foreground">
|
||||||
{open ? "Hide Examples" : "See Examples"}
|
{open ? "Hide Examples" : "See Examples"}
|
||||||
@ -41,13 +48,20 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
|
|||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)}
|
)} */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold">Options</h4>
|
<h4 className="font-semibold">Options</h4>
|
||||||
<ul className="pl-4 list-disc">
|
<ul className="pl-4 list-disc">
|
||||||
{options.map((opt) => (
|
{commission.options.map((option) => (
|
||||||
<li key={opt.key}>
|
<li key={option.id}>
|
||||||
{opt.name}: {opt.price}€
|
{option.option?.name}:{" "}
|
||||||
|
{option.price
|
||||||
|
? `${option.price}€`
|
||||||
|
: option.pricePercent
|
||||||
|
? `+${option.pricePercent}%`
|
||||||
|
: option.priceRange
|
||||||
|
? `${option.priceRange}€`
|
||||||
|
: "Included"}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -56,28 +70,28 @@ export function CommissionCard({ type, options, extras, examples }: CommissionCa
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold">Extras</h4>
|
<h4 className="font-semibold">Extras</h4>
|
||||||
<ul className="pl-4 list-disc">
|
<ul className="pl-4 list-disc">
|
||||||
{extras.map((extra) => (
|
{commission.extras.map((extra) => (
|
||||||
<li key={extra.key}>
|
<li key={extra.id}>
|
||||||
{extra.name}:{" "}
|
{extra.extra?.name}:{" "}
|
||||||
{extra.price !== undefined
|
{extra.price
|
||||||
? `${extra.price}€`
|
? `${extra.price}€`
|
||||||
: extra.pricePercent
|
: extra.pricePercent
|
||||||
? `+${extra.pricePercent}%`
|
? `+${extra.pricePercent}%`
|
||||||
: extra.priceRange
|
: extra.priceRange
|
||||||
? `${extra.priceRange.replace("#", "–")}€`
|
? `${extra.priceRange}€`
|
||||||
: "Included"}
|
: "Included"}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{extras.map((extra) => (
|
{commission.extras.map((extra) => (
|
||||||
<Badge variant="outline" key={extra.key}>
|
<Badge variant="outline" key={extra.id}>
|
||||||
{extra.name}
|
{extra.extra?.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div> */}
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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 = {
|
// export const commissionTypes = {
|
||||||
sketch: {
|
// sketch: {
|
||||||
name: 'Sketch',
|
// name: 'Sketch',
|
||||||
description: 'A quick sketch of a character',
|
// description: 'A quick sketch of a character',
|
||||||
},
|
// },
|
||||||
flat: {
|
// flat: {
|
||||||
name: 'Flat Color / Lineart',
|
// name: 'Flat Color / Lineart',
|
||||||
description: 'Dunno how to describe this',
|
// description: 'Dunno how to describe this',
|
||||||
},
|
// },
|
||||||
shaded: {
|
// shaded: {
|
||||||
name: 'Shaded',
|
// name: 'Shaded',
|
||||||
description: 'Dunno how to describe this',
|
// description: 'Dunno how to describe this',
|
||||||
},
|
// },
|
||||||
refsheet: {
|
// refsheet: {
|
||||||
name: 'Reference Sheet',
|
// name: 'Reference Sheet',
|
||||||
description: 'A reference sheet with multiple poses',
|
// description: 'A reference sheet with multiple poses',
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const commissionOptions = {
|
// export const commissionOptions = {
|
||||||
headshot: {
|
// headshot: {
|
||||||
name: 'Headshot',
|
// name: 'Headshot',
|
||||||
basePrice: 10
|
// basePrice: 10
|
||||||
},
|
// },
|
||||||
halfbody: {
|
// halfbody: {
|
||||||
name: 'Halfbody',
|
// name: 'Halfbody',
|
||||||
basePrice: 15
|
// basePrice: 15
|
||||||
},
|
// },
|
||||||
fullbody: {
|
// fullbody: {
|
||||||
name: 'Fullbody',
|
// name: 'Fullbody',
|
||||||
basePrice: 20
|
// basePrice: 20
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const commissionExtras = {
|
// export const commissionExtras = {
|
||||||
extraChar: {
|
// extraChar: {
|
||||||
name: 'Extra Character',
|
// name: 'Extra Character',
|
||||||
description: 'Add an extra character to the commission',
|
// description: 'Add an extra character to the commission',
|
||||||
pricePercent: 75,
|
// pricePercent: 75,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
basicBackground: {
|
// basicBackground: {
|
||||||
name: 'Basic Background',
|
// name: 'Basic Background',
|
||||||
description: 'Add a basic background to the commission',
|
// description: 'Add a basic background to the commission',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
abstractBackground: {
|
// abstractBackground: {
|
||||||
name: 'Abstract Background',
|
// name: 'Abstract Background',
|
||||||
description: 'Add an abstract background to the commission',
|
// description: 'Add an abstract background to the commission',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
gradientBackground: {
|
// gradientBackground: {
|
||||||
name: 'Gradient Background',
|
// name: 'Gradient Background',
|
||||||
description: 'Add an gradient background to the commission',
|
// description: 'Add an gradient background to the commission',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
customBackground: {
|
// customBackground: {
|
||||||
name: 'Custom Background',
|
// name: 'Custom Background',
|
||||||
description: 'Add a custom background to the commission',
|
// description: 'Add a custom background to the commission',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: "10#50"
|
// priceRange: "10#50"
|
||||||
},
|
// },
|
||||||
nsfw: {
|
// nsfw: {
|
||||||
name: 'NSFW',
|
// name: 'NSFW',
|
||||||
description: 'Add NSFW content to the commission',
|
// description: 'Add NSFW content to the commission',
|
||||||
pricePercent: 15,
|
// pricePercent: 15,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
normalComplexity: {
|
// normalComplexity: {
|
||||||
name: 'Light Complexity',
|
// name: 'Light Complexity',
|
||||||
description: 'Increase complexity of the commission',
|
// description: 'Increase complexity of the commission',
|
||||||
pricePercent: 15,
|
// pricePercent: 15,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
highComplexity: {
|
// highComplexity: {
|
||||||
name: 'High Complexity',
|
// name: 'High Complexity',
|
||||||
description: 'Highly increase complexity of the commission',
|
// description: 'Highly increase complexity of the commission',
|
||||||
pricePercent: 30,
|
// pricePercent: 30,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
},
|
// },
|
||||||
details: {
|
// details: {
|
||||||
name: 'Details',
|
// name: 'Details',
|
||||||
description: 'Add details to the commission (tails, eyes, ...)',
|
// description: 'Add details to the commission (tails, eyes, ...)',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 0,
|
// price: 0,
|
||||||
priceRange: "3#5"
|
// priceRange: "3#5"
|
||||||
},
|
// },
|
||||||
shading: {
|
// shading: {
|
||||||
name: 'Shading',
|
// name: 'Shading',
|
||||||
description: 'Add shading to the commission',
|
// description: 'Add shading to the commission',
|
||||||
pricePercent: 0,
|
// pricePercent: 0,
|
||||||
price: 20,
|
// price: 20,
|
||||||
priceRange: ""
|
// priceRange: ""
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const rawCommissions = [
|
// export const rawCommissions = [
|
||||||
{
|
// {
|
||||||
type: commissionTypes.sketch,
|
// type: commissionTypes.sketch,
|
||||||
options: [
|
// options: [
|
||||||
{
|
// {
|
||||||
type: commissionOptions.headshot,
|
// type: commissionOptions.headshot,
|
||||||
price: commissionOptions.headshot.basePrice
|
// price: commissionOptions.headshot.basePrice
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.halfbody,
|
// type: commissionOptions.halfbody,
|
||||||
price: commissionOptions.halfbody.basePrice
|
// price: commissionOptions.halfbody.basePrice
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.fullbody,
|
// type: commissionOptions.fullbody,
|
||||||
price: commissionOptions.fullbody.basePrice
|
// price: commissionOptions.fullbody.basePrice
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
extras: [
|
// extras: [
|
||||||
{
|
// {
|
||||||
type: commissionExtras.extraChar,
|
// type: commissionExtras.extraChar,
|
||||||
pricePercent: commissionExtras.extraChar.pricePercent
|
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.basicBackground,
|
// type: commissionExtras.basicBackground,
|
||||||
price: commissionExtras.basicBackground.price
|
// price: commissionExtras.basicBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.abstractBackground,
|
// type: commissionExtras.abstractBackground,
|
||||||
price: commissionExtras.abstractBackground.price
|
// price: commissionExtras.abstractBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.gradientBackground,
|
// type: commissionExtras.gradientBackground,
|
||||||
price: commissionExtras.gradientBackground.price
|
// price: commissionExtras.gradientBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.customBackground,
|
// type: commissionExtras.customBackground,
|
||||||
priceRange: commissionExtras.customBackground.priceRange
|
// priceRange: commissionExtras.customBackground.priceRange
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.nsfw,
|
// type: commissionExtras.nsfw,
|
||||||
pricePercent: commissionExtras.nsfw.pricePercent
|
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.normalComplexity,
|
// type: commissionExtras.normalComplexity,
|
||||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.highComplexity,
|
// type: commissionExtras.highComplexity,
|
||||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
examples: [
|
// examples: [
|
||||||
'/examples/sketch1.png'
|
// '/examples/sketch1.png'
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionTypes.flat,
|
// type: commissionTypes.flat,
|
||||||
options: [
|
// options: [
|
||||||
{
|
// {
|
||||||
type: commissionOptions.headshot,
|
// type: commissionOptions.headshot,
|
||||||
price: 18
|
// price: 18
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.halfbody,
|
// type: commissionOptions.halfbody,
|
||||||
price: 22
|
// price: 22
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.fullbody,
|
// type: commissionOptions.fullbody,
|
||||||
price: 28
|
// price: 28
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
extras: [
|
// extras: [
|
||||||
{
|
// {
|
||||||
type: commissionExtras.extraChar,
|
// type: commissionExtras.extraChar,
|
||||||
pricePercent: commissionExtras.extraChar.pricePercent
|
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.basicBackground,
|
// type: commissionExtras.basicBackground,
|
||||||
price: commissionExtras.basicBackground.price
|
// price: commissionExtras.basicBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.abstractBackground,
|
// type: commissionExtras.abstractBackground,
|
||||||
price: commissionExtras.abstractBackground.price
|
// price: commissionExtras.abstractBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.gradientBackground,
|
// type: commissionExtras.gradientBackground,
|
||||||
price: commissionExtras.gradientBackground.price
|
// price: commissionExtras.gradientBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.customBackground,
|
// type: commissionExtras.customBackground,
|
||||||
priceRange: commissionExtras.customBackground.priceRange
|
// priceRange: commissionExtras.customBackground.priceRange
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.nsfw,
|
// type: commissionExtras.nsfw,
|
||||||
pricePercent: commissionExtras.nsfw.pricePercent
|
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.normalComplexity,
|
// type: commissionExtras.normalComplexity,
|
||||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.highComplexity,
|
// type: commissionExtras.highComplexity,
|
||||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
examples: [
|
// examples: [
|
||||||
'/examples/flat1.png',
|
// '/examples/flat1.png',
|
||||||
'/examples/flat2.png'
|
// '/examples/flat2.png'
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionTypes.shaded,
|
// type: commissionTypes.shaded,
|
||||||
options: [
|
// options: [
|
||||||
{
|
// {
|
||||||
type: commissionOptions.headshot,
|
// type: commissionOptions.headshot,
|
||||||
price: 25
|
// price: 25
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.halfbody,
|
// type: commissionOptions.halfbody,
|
||||||
price: 40
|
// price: 40
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionOptions.fullbody,
|
// type: commissionOptions.fullbody,
|
||||||
price: 55
|
// price: 55
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
extras: [
|
// extras: [
|
||||||
{
|
// {
|
||||||
type: commissionExtras.extraChar,
|
// type: commissionExtras.extraChar,
|
||||||
pricePercent: commissionExtras.extraChar.pricePercent
|
// pricePercent: commissionExtras.extraChar.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.basicBackground,
|
// type: commissionExtras.basicBackground,
|
||||||
price: commissionExtras.basicBackground.price
|
// price: commissionExtras.basicBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.abstractBackground,
|
// type: commissionExtras.abstractBackground,
|
||||||
price: commissionExtras.abstractBackground.price
|
// price: commissionExtras.abstractBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.gradientBackground,
|
// type: commissionExtras.gradientBackground,
|
||||||
price: commissionExtras.gradientBackground.price
|
// price: commissionExtras.gradientBackground.price
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.customBackground,
|
// type: commissionExtras.customBackground,
|
||||||
priceRange: "20#100"
|
// priceRange: "20#100"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.nsfw,
|
// type: commissionExtras.nsfw,
|
||||||
pricePercent: commissionExtras.nsfw.pricePercent
|
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.normalComplexity,
|
// type: commissionExtras.normalComplexity,
|
||||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.highComplexity,
|
// type: commissionExtras.highComplexity,
|
||||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
examples: [
|
// examples: [
|
||||||
'/examples/shaded1.png'
|
// '/examples/shaded1.png'
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionTypes.refsheet,
|
// type: commissionTypes.refsheet,
|
||||||
options: [
|
// options: [
|
||||||
{
|
// {
|
||||||
type: commissionOptions.fullbody,
|
// type: commissionOptions.fullbody,
|
||||||
price: 25
|
// price: 25
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
extras: [
|
// extras: [
|
||||||
{
|
// {
|
||||||
type: commissionExtras.customBackground,
|
// type: commissionExtras.customBackground,
|
||||||
priceRange: "20#100"
|
// priceRange: "20#100"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.nsfw,
|
// type: commissionExtras.nsfw,
|
||||||
pricePercent: commissionExtras.nsfw.pricePercent
|
// pricePercent: commissionExtras.nsfw.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.normalComplexity,
|
// type: commissionExtras.normalComplexity,
|
||||||
pricePercent: commissionExtras.normalComplexity.pricePercent
|
// pricePercent: commissionExtras.normalComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.highComplexity,
|
// type: commissionExtras.highComplexity,
|
||||||
pricePercent: commissionExtras.highComplexity.pricePercent
|
// pricePercent: commissionExtras.highComplexity.pricePercent
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.details,
|
// type: commissionExtras.details,
|
||||||
priceRange: commissionExtras.details.priceRange
|
// priceRange: commissionExtras.details.priceRange
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
type: commissionExtras.shading,
|
// type: commissionExtras.shading,
|
||||||
price: commissionExtras.shading.price
|
// price: commissionExtras.shading.price
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
examples: [
|
// examples: [
|
||||||
'/examples/refsheet1.png'
|
// '/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({
|
// export const commissionOrderSchema = z.object({
|
||||||
type: z.enum(["sketch", "flat", "shaded", "refsheet"]),
|
// type: z.enum(["sketch", "flat", "shaded", "refsheet"]),
|
||||||
body: z.enum(["headshot", "portrait", "waistup", "fullbody"]).optional(),
|
// body: z.enum(["headshot", "portrait", "waistup", "fullbody"]).optional(),
|
||||||
extras: z.array(z.string()).optional(),
|
// extras: z.array(z.string()).optional(),
|
||||||
customExtras: z.string().optional(),
|
// customExtras: z.string().optional(),
|
||||||
description: z.string().min(10),
|
// description: z.string().min(10),
|
||||||
contact: z.string().email("Please enter a valid email"),
|
// contact: z.string().email("Please enter a valid email"),
|
||||||
referenceFiles: z.array(z.any()).optional(), // for now just accept any File[]
|
// referenceFiles: z.array(z.any()).optional(), // for now just accept any File[]
|
||||||
tosAccepted: z.literal(true), // must be checked
|
// 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 {
|
// export interface CommissionType {
|
||||||
name: string
|
// name: string
|
||||||
description: string
|
// description: string
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CommissionOptionType {
|
// export interface CommissionOptionType {
|
||||||
name: string
|
// name: string
|
||||||
basePrice: number
|
// basePrice: number
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CommissionOption {
|
// export interface CommissionOption {
|
||||||
type: CommissionOptionType
|
// type: CommissionOptionType
|
||||||
price: number
|
// price: number
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CommissionExtraType {
|
// export interface CommissionExtraType {
|
||||||
name: string
|
// name: string
|
||||||
description: string
|
// description: string
|
||||||
pricePercent?: number
|
// pricePercent?: number
|
||||||
price?: number
|
// price?: number
|
||||||
priceRange?: string
|
// priceRange?: string
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CommissionExtra {
|
// export interface CommissionExtra {
|
||||||
type: CommissionExtraType
|
// type: CommissionExtraType
|
||||||
price?: number
|
// price?: number
|
||||||
pricePercent?: number
|
// pricePercent?: number
|
||||||
priceRange?: string
|
// priceRange?: string
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface Commission {
|
// export interface Commission {
|
||||||
type: CommissionType
|
// type: CommissionType
|
||||||
options: CommissionOption[]
|
// options: CommissionOption[]
|
||||||
extras: CommissionExtra[]
|
// extras: CommissionExtra[]
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CommissionCardProps {
|
// export interface CommissionCardProps {
|
||||||
type: {
|
// type: {
|
||||||
key: string
|
// key: string
|
||||||
name: string
|
// name: string
|
||||||
description: string
|
// description: string
|
||||||
}
|
// }
|
||||||
options: {
|
// options: {
|
||||||
key: string
|
// key: string
|
||||||
name: string
|
// name: string
|
||||||
price: number
|
// price: number
|
||||||
}[]
|
// }[]
|
||||||
extras: {
|
// extras: {
|
||||||
key: string
|
// key: string
|
||||||
name: string
|
// name: string
|
||||||
description: string
|
// description: string
|
||||||
price?: number
|
// price?: number
|
||||||
pricePercent?: number
|
// pricePercent?: number
|
||||||
priceRange?: string
|
// priceRange?: string
|
||||||
}[],
|
// }[],
|
||||||
examples?: 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