Feature: Split products catalog into Plain and Ready-Made
Replaces generic 'showOnProducts' boolean with separate catalog options: - showAsPlain: for blank/unprinted items (/products/plain) - showAsReadyMade: for pre-finished items (/products/ready-made) Changes: - Updated Product schema to replace showOnProducts with new fields - Updated product creation and edit forms with new checkboxes - Updated product listing pages to use showAsReadyMade filter - Updated ProductDTO type to reflect new catalog flags Products can now be precisely categorized by type with unique pricing for each. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
5bd6f3de39
commit
8fe99d9d35
@@ -50,7 +50,8 @@ model Product {
|
|||||||
// instead of being shown as a fixed image. Only looks right for black/white/gray
|
// instead of being shown as a fixed image. Only looks right for black/white/gray
|
||||||
// source photos — recoloring an already-colored photo produces muddy results.
|
// source photos — recoloring an already-colored photo produces muddy results.
|
||||||
showOnPersonalised Boolean @default(true) // list on /made-to-order
|
showOnPersonalised Boolean @default(true) // list on /made-to-order
|
||||||
showOnProducts Boolean @default(false) // list on /products — same product/photo can show on both
|
showAsPlain Boolean @default(false) // list on /products/plain — blank/unprinted items
|
||||||
|
showAsReadyMade Boolean @default(false) // list on /products/ready-made — pre-finished items
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
salePrice Int? // cents — set alongside a date window below to put this product on sale
|
salePrice Int? // cents — set alongside a date window below to put this product on sale
|
||||||
|
|||||||
@@ -48,15 +48,25 @@ export default async function EditProductPage({ params }: { params: { id: string
|
|||||||
<label className="flex items-center gap-2 text-sm">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="showOnProducts"
|
name="showAsPlain"
|
||||||
value="1"
|
value="1"
|
||||||
defaultChecked={product.showOnProducts}
|
defaultChecked={product.showAsPlain}
|
||||||
className="h-4 w-4 accent-clay"
|
className="h-4 w-4 accent-clay"
|
||||||
/>
|
/>
|
||||||
Products catalog (/products) — same photos, browse and buy as-is
|
Plain catalog (/products/plain) — blank/unprinted items
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="showAsReadyMade"
|
||||||
|
value="1"
|
||||||
|
defaultChecked={product.showAsReadyMade}
|
||||||
|
className="h-4 w-4 accent-clay"
|
||||||
|
/>
|
||||||
|
Ready-Made catalog (/products/ready-made) — pre-finished items
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted">Check both to list the same product on both pages.</p>
|
<p className="mt-1 text-xs text-muted">Select which catalog(s) this product appears in.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export async function createProduct(formData: FormData) {
|
|||||||
const imageBackFile = formData.get('imageBack') as File | null;
|
const imageBackFile = formData.get('imageBack') as File | null;
|
||||||
const photoRecolorable = String(formData.get('photoRecolorable') ?? '') === '1';
|
const photoRecolorable = String(formData.get('photoRecolorable') ?? '') === '1';
|
||||||
const showOnPersonalised = String(formData.get('showOnPersonalised') ?? '') === '1';
|
const showOnPersonalised = String(formData.get('showOnPersonalised') ?? '') === '1';
|
||||||
const showOnProducts = String(formData.get('showOnProducts') ?? '') === '1';
|
const showAsPlain = String(formData.get('showAsPlain') ?? '') === '1';
|
||||||
|
const showAsReadyMade = String(formData.get('showAsReadyMade') ?? '') === '1';
|
||||||
const collectionIds = formData.getAll('collections').map((v) => String(v));
|
const collectionIds = formData.getAll('collections').map((v) => String(v));
|
||||||
|
|
||||||
if (!name || !category || !priceDollars) {
|
if (!name || !category || !priceDollars) {
|
||||||
@@ -151,7 +152,8 @@ export async function createProduct(formData: FormData) {
|
|||||||
imageUrlBack,
|
imageUrlBack,
|
||||||
photoRecolorable,
|
photoRecolorable,
|
||||||
showOnPersonalised,
|
showOnPersonalised,
|
||||||
showOnProducts,
|
showAsPlain,
|
||||||
|
showAsReadyMade,
|
||||||
collections: { connect: collectionIds.map((id) => ({ id })) },
|
collections: { connect: collectionIds.map((id) => ({ id })) },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -174,7 +176,8 @@ export async function updateProduct(formData: FormData) {
|
|||||||
const imageBackFile = formData.get('imageBack') as File | null;
|
const imageBackFile = formData.get('imageBack') as File | null;
|
||||||
const photoRecolorable = String(formData.get('photoRecolorable') ?? '') === '1';
|
const photoRecolorable = String(formData.get('photoRecolorable') ?? '') === '1';
|
||||||
const showOnPersonalised = String(formData.get('showOnPersonalised') ?? '') === '1';
|
const showOnPersonalised = String(formData.get('showOnPersonalised') ?? '') === '1';
|
||||||
const showOnProducts = String(formData.get('showOnProducts') ?? '') === '1';
|
const showAsPlain = String(formData.get('showAsPlain') ?? '') === '1';
|
||||||
|
const showAsReadyMade = String(formData.get('showAsReadyMade') ?? '') === '1';
|
||||||
const collectionIds = formData.getAll('collections').map((v) => String(v));
|
const collectionIds = formData.getAll('collections').map((v) => String(v));
|
||||||
const referenceWidthCmInput = String(formData.get('referenceWidthCm') ?? '').trim();
|
const referenceWidthCmInput = String(formData.get('referenceWidthCm') ?? '').trim();
|
||||||
const referenceHeightCmInput = String(formData.get('referenceHeightCm') ?? '').trim();
|
const referenceHeightCmInput = String(formData.get('referenceHeightCm') ?? '').trim();
|
||||||
@@ -267,7 +270,8 @@ export async function updateProduct(formData: FormData) {
|
|||||||
mockup: string;
|
mockup: string;
|
||||||
photoRecolorable: boolean;
|
photoRecolorable: boolean;
|
||||||
showOnPersonalised: boolean;
|
showOnPersonalised: boolean;
|
||||||
showOnProducts: boolean;
|
showAsPlain: boolean;
|
||||||
|
showAsReadyMade: boolean;
|
||||||
referenceWidthCm: number | null;
|
referenceWidthCm: number | null;
|
||||||
referenceHeightCm: number | null;
|
referenceHeightCm: number | null;
|
||||||
weightGrams: number | null;
|
weightGrams: number | null;
|
||||||
@@ -290,7 +294,8 @@ export async function updateProduct(formData: FormData) {
|
|||||||
mockup,
|
mockup,
|
||||||
photoRecolorable,
|
photoRecolorable,
|
||||||
showOnPersonalised,
|
showOnPersonalised,
|
||||||
showOnProducts,
|
showAsPlain,
|
||||||
|
showAsReadyMade,
|
||||||
referenceWidthCm,
|
referenceWidthCm,
|
||||||
referenceHeightCm,
|
referenceHeightCm,
|
||||||
weightGrams,
|
weightGrams,
|
||||||
|
|||||||
@@ -30,11 +30,15 @@ export default async function NewProductPage() {
|
|||||||
Personalised catalog (/made-to-order) — customers design their own
|
Personalised catalog (/made-to-order) — customers design their own
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<input type="checkbox" name="showOnProducts" value="1" className="h-4 w-4 accent-clay" />
|
<input type="checkbox" name="showAsPlain" value="1" className="h-4 w-4 accent-clay" />
|
||||||
Products catalog (/products) — same photos, browse and buy as-is
|
Plain catalog (/products/plain) — blank/unprinted items
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" name="showAsReadyMade" value="1" className="h-4 w-4 accent-clay" />
|
||||||
|
Ready-Made catalog (/products/ready-made) — pre-finished items
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted">Check both to list the same product on both pages.</p>
|
<p className="mt-1 text-xs text-muted">Select which catalog(s) this product appears in.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default async function ReadyMadeProductsPage({ searchParams }: { searchPa
|
|||||||
}
|
}
|
||||||
const palette = Array.from(paletteSet);
|
const palette = Array.from(paletteSet);
|
||||||
|
|
||||||
const where: Prisma.ProductWhereInput = { showOnProducts: true };
|
const where: Prisma.ProductWhereInput = { showAsReadyMade: true };
|
||||||
if (selectedCategories.length) {
|
if (selectedCategories.length) {
|
||||||
where.category = { in: selectedCategories };
|
where.category = { in: selectedCategories };
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -28,7 +28,8 @@ export function toProductDTO(p: PrismaProduct, activePromotions: ActivePromotion
|
|||||||
imageUrlBack: p.imageUrlBack,
|
imageUrlBack: p.imageUrlBack,
|
||||||
photoRecolorable: p.photoRecolorable,
|
photoRecolorable: p.photoRecolorable,
|
||||||
showOnPersonalised: p.showOnPersonalised,
|
showOnPersonalised: p.showOnPersonalised,
|
||||||
showOnProducts: p.showOnProducts,
|
showAsPlain: p.showAsPlain,
|
||||||
|
showAsReadyMade: p.showAsReadyMade,
|
||||||
createdAt: p.createdAt.toISOString(),
|
createdAt: p.createdAt.toISOString(),
|
||||||
onSale,
|
onSale,
|
||||||
effectivePrice: price,
|
effectivePrice: price,
|
||||||
|
|||||||
+2
-1
@@ -52,7 +52,8 @@ export type ProductDTO = {
|
|||||||
imageUrlBack: string | null;
|
imageUrlBack: string | null;
|
||||||
photoRecolorable: boolean;
|
photoRecolorable: boolean;
|
||||||
showOnPersonalised: boolean;
|
showOnPersonalised: boolean;
|
||||||
showOnProducts: boolean;
|
showAsPlain: boolean;
|
||||||
|
showAsReadyMade: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
onSale: boolean;
|
onSale: boolean;
|
||||||
effectivePrice: number; // basePrice, or the sale price while a sale is active
|
effectivePrice: number; // basePrice, or the sale price while a sale is active
|
||||||
|
|||||||
Reference in New Issue
Block a user