Feature: Add third price option for plain/blank products

- Added plainPrice field to Product model in Prisma schema
- Updated database to include new plainPrice column
- Added Plain price input field to product creation form (new/page.tsx)
- Added Plain price input field to product edit form ([id]/edit/page.tsx)
- Updated createProduct server action to handle plainPrice
- Updated updateProduct server action to handle plainPrice

Now supports three price tiers:
- basePrice: for ready-made items
- plainPrice: for plain/blank items (new)
- personalisedPrice: for made-to-order items

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-20 18:00:06 +01:00
co-authored by Claude Haiku 4.5
parent efa0bfa03e
commit e5fb22aab1
5 changed files with 78 additions and 35 deletions
+7 -1
View File
@@ -128,7 +128,13 @@
"Bash(git commit -m 'Chore: Migrate database from SQLite to PostgreSQL on VM *)", "Bash(git commit -m 'Chore: Migrate database from SQLite to PostgreSQL on VM *)",
"Bash(git pull *)", "Bash(git pull *)",
"Bash(git rm *)", "Bash(git rm *)",
"Bash(git commit -m 'Chore: Remove .env from version control for security *)" "Bash(git commit -m 'Chore: Remove .env from version control for security *)",
"Bash(git fetch *)",
"Bash(git ls-remote *)",
"Bash(timeout 30 git push origin main --force)",
"Bash(GIT_TRACE=1 git push origin main --force)",
"Bash(pkill -f \"node.exe\")",
"PowerShell(Stop-Process -Name node -Force -ErrorAction SilentlyContinue)"
] ]
} }
} }
+1
View File
@@ -26,6 +26,7 @@ model Product {
category String category String
description String description String
basePrice Int // cents — used for ready-made items on /products basePrice Int // cents — used for ready-made items on /products
plainPrice Int? // cents — used for plain/blank items
personalisedPrice Int? // cents — used for made-to-order items on /made-to-order; falls back to basePrice if not set personalisedPrice Int? // cents — used for made-to-order items on /made-to-order; falls back to basePrice if not set
colors String // JSON array of hex strings, e.g. ["#17181C","#FBF9F5"] colors String // JSON array of hex strings, e.g. ["#17181C","#FBF9F5"]
colorPhotos String? // JSON object mapping hex -> photo data URL, e.g. {"#17181C":"data:..."}. colorPhotos String? // JSON object mapping hex -> photo data URL, e.g. {"#17181C":"data:..."}.
+19 -3
View File
@@ -220,6 +220,23 @@ export default async function EditProductPage({ params }: { params: { id: string
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="plainPrice" className="tag-label mb-2 block">
Plain price (£, optional)
</label>
<input
id="plainPrice"
name="plainPrice"
type="number"
step="0.01"
min={0}
defaultValue={row.plainPrice != null ? (row.plainPrice / 100).toFixed(2) : ''}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="Leave blank if not applicable"
/>
<p className="mt-1 text-xs text-muted">For plain/blank items</p>
</div>
<div> <div>
<label htmlFor="personalisedPrice" className="tag-label mb-2 block"> <label htmlFor="personalisedPrice" className="tag-label mb-2 block">
Personalised price (£, optional) Personalised price (£, optional)
@@ -234,9 +251,8 @@ export default async function EditProductPage({ params }: { params: { id: string
className="w-full border border-line bg-paper px-3 py-2 text-sm" className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="Leave blank to use the regular price" placeholder="Leave blank to use the regular price"
/> />
<p className="mt-1 text-xs text-muted"> <p className="mt-1 text-xs text-muted">For made-to-order items</p>
Higher price for made-to-order items. Leave blank to use the same price as ready-made. </div>
</p>
</div> </div>
<div> <div>
+5
View File
@@ -67,6 +67,7 @@ export async function createProduct(formData: FormData) {
const category = String(formData.get('category') ?? ''); const category = String(formData.get('category') ?? '');
const description = String(formData.get('description') ?? '').trim(); const description = String(formData.get('description') ?? '').trim();
const priceDollars = Number(formData.get('price') ?? 0); const priceDollars = Number(formData.get('price') ?? 0);
const plainPriceDollars = String(formData.get('plainPrice') ?? '').trim();
const personalisedPriceDollars = String(formData.get('personalisedPrice') ?? '').trim(); const personalisedPriceDollars = String(formData.get('personalisedPrice') ?? '').trim();
const mockup = String(formData.get('mockup') ?? 'tshirt'); const mockup = String(formData.get('mockup') ?? 'tshirt');
const colorsInput = String(formData.get('colors') ?? ''); const colorsInput = String(formData.get('colors') ?? '');
@@ -135,6 +136,7 @@ export async function createProduct(formData: FormData) {
category, category,
description: description || `${name} — personalize it your way.`, description: description || `${name} — personalize it your way.`,
basePrice: Math.round(priceDollars * 100), basePrice: Math.round(priceDollars * 100),
plainPrice: plainPriceDollars ? Math.round(Number(plainPriceDollars) * 100) : null,
personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null, personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null,
weightGrams, weightGrams,
...saleFields, ...saleFields,
@@ -163,6 +165,7 @@ export async function updateProduct(formData: FormData) {
const category = String(formData.get('category') ?? ''); const category = String(formData.get('category') ?? '');
const description = String(formData.get('description') ?? '').trim(); const description = String(formData.get('description') ?? '').trim();
const priceDollars = Number(formData.get('price') ?? 0); const priceDollars = Number(formData.get('price') ?? 0);
const plainPriceDollars = String(formData.get('plainPrice') ?? '').trim();
const personalisedPriceDollars = String(formData.get('personalisedPrice') ?? '').trim(); const personalisedPriceDollars = String(formData.get('personalisedPrice') ?? '').trim();
const mockup = String(formData.get('mockup') ?? 'tshirt'); const mockup = String(formData.get('mockup') ?? 'tshirt');
const colorsInput = String(formData.get('colors') ?? ''); const colorsInput = String(formData.get('colors') ?? '');
@@ -252,6 +255,7 @@ export async function updateProduct(formData: FormData) {
category: string; category: string;
description: string; description: string;
basePrice: number; basePrice: number;
plainPrice: number | null;
personalisedPrice: number | null; personalisedPrice: number | null;
salePrice: number | null; salePrice: number | null;
saleStartsAt: Date | null; saleStartsAt: Date | null;
@@ -276,6 +280,7 @@ export async function updateProduct(formData: FormData) {
category, category,
description: description || `${name} — personalize it your way.`, description: description || `${name} — personalize it your way.`,
basePrice: Math.round(priceDollars * 100), basePrice: Math.round(priceDollars * 100),
plainPrice: plainPriceDollars ? Math.round(Number(plainPriceDollars) * 100) : null,
personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null, personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null,
...parseSaleFields(formData), ...parseSaleFields(formData),
colors: JSON.stringify(colors), colors: JSON.stringify(colors),
+19 -4
View File
@@ -21,7 +21,7 @@ export default async function NewProductPage() {
design their own version, the ready-made one where they just buy it as pictured, or both. design their own version, the ready-made one where they just buy it as pictured, or both.
</p> </p>
<form action={createProduct} className="mt-10 space-y-6" encType="multipart/form-data"> <form action={createProduct} className="mt-10 space-y-6">
<div> <div>
<label className="tag-label mb-2 block">Show on</label> <label className="tag-label mb-2 block">Show on</label>
<div className="space-y-2"> <div className="space-y-2">
@@ -196,6 +196,22 @@ export default async function NewProductPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="plainPrice" className="tag-label mb-2 block">
Plain price (£, optional)
</label>
<input
id="plainPrice"
name="plainPrice"
type="number"
step="0.01"
min={0}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="Leave blank if not applicable"
/>
<p className="mt-1 text-xs text-muted">For plain/blank items</p>
</div>
<div> <div>
<label htmlFor="personalisedPrice" className="tag-label mb-2 block"> <label htmlFor="personalisedPrice" className="tag-label mb-2 block">
Personalised price (£, optional) Personalised price (£, optional)
@@ -209,9 +225,8 @@ export default async function NewProductPage() {
className="w-full border border-line bg-paper px-3 py-2 text-sm" className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="Leave blank to use the regular price" placeholder="Leave blank to use the regular price"
/> />
<p className="mt-1 text-xs text-muted"> <p className="mt-1 text-xs text-muted">For made-to-order items</p>
Higher price for made-to-order items. Leave blank to use the same price as ready-made. </div>
</p>
</div> </div>
<div> <div>