Add: Weight field to product admin pages

Added weight (in kilograms) field to both create and edit product pages.
Admins can now set the weight for each product, which is converted to
grams and stored in the database for Royal Mail shipping calculations.
This commit is contained in:
Andymick
2026-07-19 06:50:23 +01:00
parent e150f2aed9
commit f92ed2709c
4 changed files with 50 additions and 1 deletions
+2 -1
View File
@@ -86,7 +86,8 @@
"Bash(git commit -m 'Feature: Add product selection when starting design from photo request *)", "Bash(git commit -m 'Feature: Add product selection when starting design from photo request *)",
"Bash(git commit -m 'Refactor: Move start design server action to separate file *)", "Bash(git commit -m 'Refactor: Move start design server action to separate file *)",
"Bash(git commit -m 'Add: Prominent '\\\\''Shop plain items'\\\\'' section to homepage *)", "Bash(git commit -m 'Add: Prominent '\\\\''Shop plain items'\\\\'' section to homepage *)",
"Bash(git commit -m 'Feature: Royal Mail shipping integration with weight-based postage *)" "Bash(git commit -m 'Feature: Royal Mail shipping integration with weight-based postage *)",
"Bash(git commit -m 'Add: Weight field to product admin pages *)"
] ]
} }
} }
+19
View File
@@ -239,6 +239,25 @@ export default async function EditProductPage({ params }: { params: { id: string
</p> </p>
</div> </div>
<div>
<label htmlFor="weightKg" className="tag-label mb-2 block">
Weight (kg, optional)
</label>
<input
id="weightKg"
name="weightKg"
type="number"
step="0.01"
min={0}
defaultValue={row.weightGrams != null ? (row.weightGrams / 1000).toFixed(2) : ''}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="e.g. 0.25"
/>
<p className="mt-1 text-xs text-muted">
Used for Royal Mail shipping cost calculation at checkout. Enter weight in kilograms.
</p>
</div>
<div> <div>
<label className="tag-label mb-2 block">Front photo</label> <label className="tag-label mb-2 block">Front photo</label>
{product.imageUrl && ( {product.imageUrl && (
+11
View File
@@ -96,6 +96,10 @@ export async function createProduct(formData: FormData) {
.map((s) => s.trim()) .map((s) => s.trim())
.filter(Boolean); .filter(Boolean);
// Weight in kilograms converted to grams
const weightKgInput = String(formData.get('weightKg') ?? '').trim();
const weightGrams = weightKgInput ? Math.round(Number(weightKgInput) * 1000) : null;
const imageUrl = imageFile && imageFile.size > 0 ? await fileToDataUrl(imageFile) : null; const imageUrl = imageFile && imageFile.size > 0 ? await fileToDataUrl(imageFile) : null;
const imageUrlBack = imageBackFile && imageBackFile.size > 0 ? await fileToDataUrl(imageBackFile) : null; const imageUrlBack = imageBackFile && imageBackFile.size > 0 ? await fileToDataUrl(imageBackFile) : null;
const colorPhotos = await extractColorPhotos(formData, colors, 'front'); const colorPhotos = await extractColorPhotos(formData, colors, 'front');
@@ -132,6 +136,7 @@ export async function createProduct(formData: FormData) {
description: description || `${name} — personalize it your way.`, description: description || `${name} — personalize it your way.`,
basePrice: Math.round(priceDollars * 100), basePrice: Math.round(priceDollars * 100),
personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null, personalisedPrice: personalisedPriceDollars ? Math.round(Number(personalisedPriceDollars) * 100) : null,
weightGrams,
...saleFields, ...saleFields,
colors: JSON.stringify(colors), colors: JSON.stringify(colors),
colorPhotos: Object.keys(colorPhotos).length > 0 ? JSON.stringify(colorPhotos) : null, colorPhotos: Object.keys(colorPhotos).length > 0 ? JSON.stringify(colorPhotos) : null,
@@ -173,6 +178,10 @@ export async function updateProduct(formData: FormData) {
const referenceWidthCm = referenceWidthCmInput ? Number(referenceWidthCmInput) : null; const referenceWidthCm = referenceWidthCmInput ? Number(referenceWidthCmInput) : null;
const referenceHeightCm = referenceHeightCmInput ? Number(referenceHeightCmInput) : null; const referenceHeightCm = referenceHeightCmInput ? Number(referenceHeightCmInput) : null;
// Weight in kilograms converted to grams
const weightKgInput = String(formData.get('weightKg') ?? '').trim();
const weightGrams = weightKgInput ? Math.round(Number(weightKgInput) * 1000) : null;
if (!id) throw new Error('Missing product id.'); if (!id) throw new Error('Missing product id.');
if (!name || !category || !priceDollars) { if (!name || !category || !priceDollars) {
throw new Error('Name, category, and price are required.'); throw new Error('Name, category, and price are required.');
@@ -234,6 +243,7 @@ export async function updateProduct(formData: FormData) {
showOnProducts: boolean; showOnProducts: boolean;
referenceWidthCm: number | null; referenceWidthCm: number | null;
referenceHeightCm: number | null; referenceHeightCm: number | null;
weightGrams: number | null;
imageUrl?: string; imageUrl?: string;
imageUrlBack?: string; imageUrlBack?: string;
printArea?: string; printArea?: string;
@@ -255,6 +265,7 @@ export async function updateProduct(formData: FormData) {
showOnProducts, showOnProducts,
referenceWidthCm, referenceWidthCm,
referenceHeightCm, referenceHeightCm,
weightGrams,
}; };
// Only touch the front photo/print area if a new photo was actually uploaded — // Only touch the front photo/print area if a new photo was actually uploaded —
+18
View File
@@ -214,6 +214,24 @@ export default async function NewProductPage() {
</p> </p>
</div> </div>
<div>
<label htmlFor="weightKg" className="tag-label mb-2 block">
Weight (kg, optional)
</label>
<input
id="weightKg"
name="weightKg"
type="number"
step="0.01"
min={0}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
placeholder="e.g. 0.25"
/>
<p className="mt-1 text-xs text-muted">
Used for Royal Mail shipping cost calculation at checkout. Enter weight in kilograms.
</p>
</div>
<div> <div>
<label className="tag-label mb-2 block">Front photo (optional)</label> <label className="tag-label mb-2 block">Front photo (optional)</label>
<PhotoPrintAreaField /> <PhotoPrintAreaField />