Add website slot defaults including leather banner placeholder.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
0f80de1f05
commit
00b5d85f91
@@ -16,6 +16,7 @@
|
|||||||
"db:down": "docker compose down",
|
"db:down": "docker compose down",
|
||||||
"create-super-admin": "ts-node -r tsconfig-paths/register scripts/create-super-admin.ts",
|
"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: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"
|
"send-sms": "ts-node -r tsconfig-paths/register scripts/send-sms.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -15,6 +15,12 @@ export type WebsiteSlotDef = {
|
|||||||
/** width / height for upload preview box */
|
/** width / height for upload preview box */
|
||||||
aspectRatio: number;
|
aspectRatio: number;
|
||||||
aspectLabel: string;
|
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) */
|
/** Optional labels for fixed-set cells (per-cell AR when images differ) */
|
||||||
cellLabels?: Array<{
|
cellLabels?: Array<{
|
||||||
labelFa: string;
|
labelFa: string;
|
||||||
@@ -23,6 +29,11 @@ export type WebsiteSlotDef = {
|
|||||||
aspectLabel?: string;
|
aspectLabel?: string;
|
||||||
/** Relative preview width in rem (matches on-site scale) */
|
/** Relative preview width in rem (matches on-site scale) */
|
||||||
previewWidthRem?: number;
|
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).
|
* Homepage CategoryBento (lg): 6×3 grid inside a 2:1 frame.
|
||||||
* Cell sizes at 1520×560 content (max-w 1600 − lg padding, 8px gaps):
|
* Order: mantel, shirts, pants, bag, scarf, shoe.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
slug: 'category-strip',
|
slug: 'category-strip',
|
||||||
labelFa: 'دستهبندیها',
|
labelFa: 'دستهبندیها',
|
||||||
labelEn: 'Categories',
|
labelEn: 'Categories',
|
||||||
kind: 'fixed-set',
|
kind: 'fixed-set',
|
||||||
itemCount: 5,
|
itemCount: 6,
|
||||||
aspectRatio: 501 / 560,
|
aspectRatio: 1,
|
||||||
aspectLabel: 'بنتو · ۵ قاب',
|
aspectLabel: 'بنتو · ۶ قاب',
|
||||||
cellLabels: [
|
cellLabels: [
|
||||||
{
|
{
|
||||||
labelFa: 'قاب بلند',
|
labelFa: 'مانتو',
|
||||||
labelEn: 'Tall',
|
labelEn: 'Mantel',
|
||||||
aspectRatio: 501 / 560,
|
aspectRatio: 1,
|
||||||
aspectLabel: '501×560',
|
aspectLabel: '۱×۱',
|
||||||
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',
|
|
||||||
previewWidthRem: 8,
|
previewWidthRem: 8,
|
||||||
|
defaultImage: '/images/transparent/mantel.png',
|
||||||
|
defaultNameFa: 'مانتو',
|
||||||
|
defaultNameEn: 'Mantel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelFa: 'قاب پایین',
|
labelFa: 'پیراهن',
|
||||||
labelEn: 'Bottom',
|
labelEn: 'Shirts',
|
||||||
aspectRatio: 501 / 181,
|
aspectRatio: 4,
|
||||||
aspectLabel: '501×181',
|
aspectLabel: '۴×۱',
|
||||||
|
previewWidthRem: 14,
|
||||||
|
defaultImage: '/images/transparent/shirt.png',
|
||||||
|
defaultNameFa: 'پیراهن',
|
||||||
|
defaultNameEn: 'Shirts',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelFa: 'شلوار',
|
||||||
|
labelEn: 'Pants',
|
||||||
|
aspectRatio: 2,
|
||||||
|
aspectLabel: '۲×۱',
|
||||||
previewWidthRem: 9,
|
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',
|
kind: 'fixed',
|
||||||
aspectRatio: 1920 / 585,
|
aspectRatio: 1920 / 585,
|
||||||
aspectLabel: '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',
|
slug: 'four-portrait-banner',
|
||||||
|
|||||||
Reference in New Issue
Block a user