diff --git a/package.json b/package.json index d4787c4..877ec6d 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/seed-website-slot-defaults.ts b/scripts/seed-website-slot-defaults.ts new file mode 100644 index 0000000..831f500 --- /dev/null +++ b/scripts/seed-website-slot-defaults.ts @@ -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(); + }); diff --git a/src/website-slots/website-slot.catalog.ts b/src/website-slots/website-slot.catalog.ts index 3e031a0..132c0e7 100644 --- a/src/website-slots/website-slot.catalog.ts +++ b/src/website-slots/website-slot.catalog.ts @@ -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',