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:
co-authored by
Claude Haiku 4.5
parent
efa0bfa03e
commit
e5fb22aab1
@@ -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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:..."}.
|
||||||
|
|||||||
@@ -220,23 +220,39 @@ export default async function EditProductPage({ params }: { params: { id: string
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<label htmlFor="personalisedPrice" className="tag-label mb-2 block">
|
<div>
|
||||||
Personalised price (£, optional)
|
<label htmlFor="plainPrice" className="tag-label mb-2 block">
|
||||||
</label>
|
Plain price (£, optional)
|
||||||
<input
|
</label>
|
||||||
id="personalisedPrice"
|
<input
|
||||||
name="personalisedPrice"
|
id="plainPrice"
|
||||||
type="number"
|
name="plainPrice"
|
||||||
step="0.01"
|
type="number"
|
||||||
min={0}
|
step="0.01"
|
||||||
defaultValue={row.personalisedPrice != null ? (row.personalisedPrice / 100).toFixed(2) : ''}
|
min={0}
|
||||||
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
defaultValue={row.plainPrice != null ? (row.plainPrice / 100).toFixed(2) : ''}
|
||||||
placeholder="Leave blank to use the regular price"
|
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">
|
/>
|
||||||
Higher price for made-to-order items. Leave blank to use the same price as ready-made.
|
<p className="mt-1 text-xs text-muted">For plain/blank items</p>
|
||||||
</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="personalisedPrice" className="tag-label mb-2 block">
|
||||||
|
Personalised price (£, optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="personalisedPrice"
|
||||||
|
name="personalisedPrice"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min={0}
|
||||||
|
defaultValue={row.personalisedPrice != null ? (row.personalisedPrice / 100).toFixed(2) : ''}
|
||||||
|
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
||||||
|
placeholder="Leave blank to use the regular price"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted">For made-to-order items</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,22 +196,37 @@ export default async function NewProductPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<label htmlFor="personalisedPrice" className="tag-label mb-2 block">
|
<div>
|
||||||
Personalised price (£, optional)
|
<label htmlFor="plainPrice" className="tag-label mb-2 block">
|
||||||
</label>
|
Plain price (£, optional)
|
||||||
<input
|
</label>
|
||||||
id="personalisedPrice"
|
<input
|
||||||
name="personalisedPrice"
|
id="plainPrice"
|
||||||
type="number"
|
name="plainPrice"
|
||||||
step="0.01"
|
type="number"
|
||||||
min={0}
|
step="0.01"
|
||||||
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
min={0}
|
||||||
placeholder="Leave blank to use the regular price"
|
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">
|
/>
|
||||||
Higher price for made-to-order items. Leave blank to use the same price as ready-made.
|
<p className="mt-1 text-xs text-muted">For plain/blank items</p>
|
||||||
</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="personalisedPrice" className="tag-label mb-2 block">
|
||||||
|
Personalised price (£, optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="personalisedPrice"
|
||||||
|
name="personalisedPrice"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min={0}
|
||||||
|
className="w-full border border-line bg-paper px-3 py-2 text-sm"
|
||||||
|
placeholder="Leave blank to use the regular price"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted">For made-to-order items</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user