Add website slot defaults including leather banner placeholder.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-11 15:02:41 +03:30
co-authored by Cursor
parent 0f80de1f05
commit 00b5d85f91
3 changed files with 286 additions and 36 deletions
+1
View File
@@ -16,6 +16,7 @@
"db:down": "docker compose down",
"create-super-admin": "ts-node -r tsconfig-paths/register scripts/create-super-admin.ts",
"seed:mana-store": "ts-node -r tsconfig-paths/register scripts/seed-mana-store.ts",
"seed:website-slots": "ts-node -r tsconfig-paths/register scripts/seed-website-slot-defaults.ts",
"send-sms": "ts-node -r tsconfig-paths/register scripts/send-sms.ts"
},
"dependencies": {
+139
View File
@@ -0,0 +1,139 @@
/**
* Seed curated homepage images into website slots for mana-collection.
* Creates missing cells and fills empty imageUrl with storefront defaults.
* Never overwrites an existing uploaded imageUrl.
*
* Usage: npm run seed:website-slots
*/
import { PrismaClient } from '@prisma/client';
import { WEBSITE_SLOT_DEFS } from '../src/website-slots/website-slot.catalog';
const prisma = new PrismaClient();
type DefaultRow = {
slotKey: string;
sortOrder: number;
nameFa: string;
nameEn: string;
subtext: string;
link: string;
imageUrl: string;
};
function collectDefaults(): DefaultRow[] {
const rows: DefaultRow[] = [];
for (const def of WEBSITE_SLOT_DEFS) {
if (def.kind === 'fixed' && def.defaultImage) {
rows.push({
slotKey: def.slug,
sortOrder: 0,
nameFa: def.defaultNameFa || def.labelFa,
nameEn: def.defaultNameEn || def.labelEn,
subtext: def.defaultSubtext || '',
link: def.defaultLink || '',
imageUrl: def.defaultImage,
});
continue;
}
if (def.kind === 'fixed-set' && def.cellLabels?.length) {
def.cellLabels.forEach((cell, index) => {
if (!cell.defaultImage) return;
rows.push({
slotKey: def.slug,
sortOrder: index,
nameFa: cell.defaultNameFa || cell.labelFa,
nameEn: cell.defaultNameEn || cell.labelEn,
subtext: cell.defaultSubtext || '',
link: cell.defaultLink || '',
imageUrl: cell.defaultImage,
});
});
}
}
return rows;
}
async function main() {
const store = await prisma.store.findFirst({
where: { slug: 'mana-collection' },
select: { id: true, slug: true },
});
if (!store) {
throw new Error('Store mana-collection not found. Run seed:mana-store first.');
}
const defaults = collectDefaults();
let created = 0;
let filled = 0;
let skipped = 0;
for (const row of defaults) {
const existing = await prisma.websiteSlotItem.findFirst({
where: {
storeId: store.id,
slotKey: row.slotKey,
sortOrder: row.sortOrder,
},
});
if (!existing) {
await prisma.websiteSlotItem.create({
data: {
storeId: store.id,
slotKey: row.slotKey,
sortOrder: row.sortOrder,
nameFa: row.nameFa,
nameEn: row.nameEn,
subtext: row.subtext,
linkText: '',
link: row.link,
imageUrl: row.imageUrl,
},
});
created += 1;
continue;
}
const shouldReplaceImage =
!existing.imageUrl ||
// Replace legacy CDN category icons with curated transparent assets.
(row.slotKey === 'category-strip' &&
!existing.imageUrl.includes('/images/transparent/')) ||
((row.slotKey === 'opening-frame' || row.slotKey === 'sticky-story') &&
!existing.imageUrl.includes('/images/landscapes/'));
if (shouldReplaceImage) {
await prisma.websiteSlotItem.update({
where: { id: existing.id },
data: {
imageUrl: row.imageUrl,
nameFa: existing.nameFa || row.nameFa,
nameEn: existing.nameEn || row.nameEn,
subtext: existing.subtext || row.subtext,
link: existing.link || row.link,
},
});
filled += 1;
continue;
}
skipped += 1;
}
console.log('Seeded website slot defaults:', {
store: store.slug,
created,
filledEmptyImages: filled,
alreadyHadImages: skipped,
totalDefaults: defaults.length,
});
}
main()
.catch((err) => {
console.error(err);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});
+146 -36
View File
@@ -15,6 +15,12 @@ export type WebsiteSlotDef = {
/** width / height for upload preview box */
aspectRatio: number;
aspectLabel: string;
/** Storefront-relative default image when no upload exists yet */
defaultImage?: string;
defaultNameFa?: string;
defaultNameEn?: string;
defaultSubtext?: string;
defaultLink?: string;
/** Optional labels for fixed-set cells (per-cell AR when images differ) */
cellLabels?: Array<{
labelFa: string;
@@ -23,6 +29,11 @@ export type WebsiteSlotDef = {
aspectLabel?: string;
/** Relative preview width in rem (matches on-site scale) */
previewWidthRem?: number;
defaultImage?: string;
defaultNameFa?: string;
defaultNameEn?: string;
defaultSubtext?: string;
defaultLink?: string;
}>;
};
@@ -37,53 +48,148 @@ export const WEBSITE_SLOT_DEFS: WebsiteSlotDef[] = [
},
{
/**
* Homepage CategoryBento (lg): 3×3 grid, height min(62vw, 560px).
* Cell sizes at 1520×560 content (max-w 1600 − lg padding, 8px gaps):
* 0: 1×3 tall · 1: 2×1 wide · 2: 1×1 · 3: 1×2 · 4: 1×1
* Mobile tiles are all aspect 4/5 — crop for desktop; cover crops on mobile.
* Homepage CategoryBento (lg): 6×3 grid inside a 2:1 frame.
* Order: mantel, shirts, pants, bag, scarf, shoe.
*/
slug: 'category-strip',
labelFa: 'دسته‌بندی‌ها',
labelEn: 'Categories',
kind: 'fixed-set',
itemCount: 5,
aspectRatio: 501 / 560,
aspectLabel: 'بنتو · ۵ قاب',
itemCount: 6,
aspectRatio: 1,
aspectLabel: 'بنتو · ۶ قاب',
cellLabels: [
{
labelFa: 'قاب بلند',
labelEn: 'Tall',
aspectRatio: 501 / 560,
aspectLabel: '501×560',
previewWidthRem: 7,
},
{
labelFa: 'قاب عریض',
labelEn: 'Wide',
aspectRatio: 1011 / 181,
aspectLabel: '1011×181',
previewWidthRem: 16,
},
{
labelFa: 'قاب میانی',
labelEn: 'Middle',
aspectRatio: 501 / 181,
aspectLabel: '501×181',
previewWidthRem: 9,
},
{
labelFa: 'قاب نیمه‌بلند',
labelEn: 'Half-tall',
aspectRatio: 501 / 371,
aspectLabel: '501×371',
labelFa: 'مانتو',
labelEn: 'Mantel',
aspectRatio: 1,
aspectLabel: '۱×۱',
previewWidthRem: 8,
defaultImage: '/images/transparent/mantel.png',
defaultNameFa: 'مانتو',
defaultNameEn: 'Mantel',
},
{
labelFa: 'قاب پایین',
labelEn: 'Bottom',
aspectRatio: 501 / 181,
aspectLabel: '501×181',
labelFa: 'پیراهن',
labelEn: 'Shirts',
aspectRatio: 4,
aspectLabel: '۴×۱',
previewWidthRem: 14,
defaultImage: '/images/transparent/shirt.png',
defaultNameFa: 'پیراهن',
defaultNameEn: 'Shirts',
},
{
labelFa: 'شلوار',
labelEn: 'Pants',
aspectRatio: 2,
aspectLabel: '۲×۱',
previewWidthRem: 9,
defaultImage: '/images/transparent/pants.png',
defaultNameFa: 'شلوار',
defaultNameEn: 'Pants',
},
{
labelFa: 'کیف زنانه',
labelEn: 'Bag',
aspectRatio: 2,
aspectLabel: '۲×۱',
previewWidthRem: 9,
defaultImage: '/images/transparent/bag.png',
defaultNameFa: 'کیف زنانه',
defaultNameEn: 'Wemon Bag',
},
{
labelFa: 'شال',
labelEn: 'Scarf',
aspectRatio: 3,
aspectLabel: '۳×۱',
previewWidthRem: 11,
defaultImage: '/images/transparent/scarf.png',
defaultNameFa: 'شال',
defaultNameEn: 'Scarf',
},
{
labelFa: 'کفش',
labelEn: 'Shoe',
aspectRatio: 3,
aspectLabel: '۳×۱',
previewWidthRem: 11,
defaultImage: '/images/transparent/shoe.png',
defaultNameFa: 'کفش',
defaultNameEn: 'Shoe',
},
],
},
{
slug: 'opening-frame',
labelFa: 'قاب بازشو',
labelEn: 'Opening frame',
kind: 'fixed',
sectionKey: 'sticky-frames',
sectionLabelFa: 'قاب‌های اسکرولی',
sectionLabelEn: 'Sticky frames',
aspectRatio: 16 / 9,
aspectLabel: '۱۶×۹',
defaultImage: '/images/landscapes/frame1.jpg',
defaultNameFa: 'به سبک مانا',
defaultNameEn: 'Opening frame',
},
{
slug: 'sticky-story',
labelFa: 'داستان اسکرولی',
labelEn: 'Sticky story',
kind: 'fixed-set',
sectionKey: 'sticky-frames',
sectionLabelFa: 'قاب‌های اسکرولی',
sectionLabelEn: 'Sticky frames',
itemCount: 4,
aspectRatio: 16 / 9,
aspectLabel: '۱۶×۹',
cellLabels: [
{
labelFa: 'مانتو',
labelEn: 'Mantel',
defaultImage:
'/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg',
defaultNameFa: 'مانتو',
defaultNameEn: 'Mantel',
defaultSubtext:
'مانتوهایی با برش معاصر؛ ساده، شهری و مناسب هر روز.',
defaultLink: '/products?category=coat',
},
{
labelFa: 'پیراهن',
labelEn: 'Shirts',
defaultImage:
'/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg',
defaultNameFa: 'پیراهن',
defaultNameEn: 'Shirts',
defaultSubtext:
'پیراهن‌هایی با پارچه لطیف و ایستایی تمیز برای استایل روزمره.',
defaultLink: '/products?category=shirts',
},
{
labelFa: 'شلوار',
labelEn: 'Pants',
defaultImage:
'/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg',
defaultNameFa: 'شلوار',
defaultNameEn: 'Pants',
defaultSubtext:
'شلوارهایی با فرم خوش‌ایست که شکل خود را حفظ می‌کنند.',
defaultLink: '/products?category=pants',
},
{
labelFa: 'کیف زنانه',
labelEn: 'Bags',
defaultImage:
'/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg',
defaultNameFa: 'کیف زنانه',
defaultNameEn: 'Bags',
defaultSubtext:
'کیف‌هایی با جزئیات دقیق؛ تکمیل‌کننده استایل مانا.',
defaultLink: '/products?category=bags',
},
],
},
@@ -127,6 +233,10 @@ export const WEBSITE_SLOT_DEFS: WebsiteSlotDef[] = [
kind: 'fixed',
aspectRatio: 1920 / 585,
aspectLabel: '1920×585',
defaultImage: '/images/home/banners/leather.png',
defaultNameFa: 'هنر پوشیدن',
defaultNameEn: 'THE ART OF DRESSING',
defaultLink: '/products?collection=leather',
},
{
slug: 'four-portrait-banner',