diff --git a/src/app/customers/page.tsx b/src/app/customers/page.tsx index 9072e29..a32889c 100644 --- a/src/app/customers/page.tsx +++ b/src/app/customers/page.tsx @@ -31,7 +31,7 @@ import { updateCustomerRole, } from "@/lib/customers-api"; -const PAGE_SIZE = 5; +const PAGE_SIZE = 24; const ROLES: UserRole[] = ["CUSTOMER", "STORE_ADMIN", "SUPER_ADMIN"]; const MEMBERSHIPS: SubscriptionStatus[] = ["regular", "premium"]; @@ -275,7 +275,7 @@ export default function CustomersPage() { style={{ animationDelay: `${70 + i * 45}ms` }} >
-

+

{customer.nameFa}

{customer.phone}

-

- {roleLabel(customer.role)} -

diff --git a/src/app/globals.css b/src/app/globals.css index d4f5e88..6411e5c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -753,11 +753,11 @@ body { display: flex; align-items: center; flex-wrap: wrap; - gap: 1rem; + gap: 0.75rem; border: 1px solid var(--line); background: #fff; border-radius: var(--radius-card); - padding: 0.9rem 1rem; + padding: 0.5rem 0.85rem; transition: border-color 0.25s ease; } diff --git a/src/app/website/images/page.tsx b/src/app/website/images/page.tsx index fae9c94..2ac25d1 100644 --- a/src/app/website/images/page.tsx +++ b/src/app/website/images/page.tsx @@ -20,6 +20,7 @@ import { groupSlotSections, heroPreviewObjectPosition, normalizeSlotDef, + resolveSlotImageUrl, type WebsiteSlotDef, type WebsiteSlotItem, } from "@/data/website-slots"; @@ -71,9 +72,16 @@ export default function WebsiteImagesPage() { setError(null); try { const data = await fetchWebsiteSlots(); - const nextCatalog = (data.catalog?.length ? data.catalog : WEBSITE_SLOT_DEFS) + const remote = (data.catalog?.length ? data.catalog : WEBSITE_SLOT_DEFS) .map((row) => normalizeSlotDef(row as WebsiteSlotDef & { key?: string })) .filter((row) => row.slug && row.slug !== "payment-badges"); + const remoteSlugs = new Set(remote.map((row) => row.slug)); + const nextCatalog = [ + ...remote, + ...WEBSITE_SLOT_DEFS.filter((row) => !remoteSlugs.has(row.slug)).map( + (row) => normalizeSlotDef(row), + ), + ]; setCatalog(nextCatalog.length ? nextCatalog : WEBSITE_SLOT_DEFS); setItems(data.items ?? []); } catch (err) { @@ -147,10 +155,14 @@ export default function WebsiteImagesPage() { setTargetSortOrder(sortOrder); const cell = sortOrder !== undefined ? def.cellLabels?.[sortOrder] : undefined; + const defaultImage = cell?.defaultImage ?? def.defaultImage ?? ""; setForm({ ...emptyForm(), - nameFa: cell?.labelFa ?? "", - nameEn: cell?.labelEn ?? "", + nameFa: cell?.defaultNameFa ?? cell?.labelFa ?? def.defaultNameFa ?? "", + nameEn: cell?.defaultNameEn ?? cell?.labelEn ?? def.defaultNameEn ?? "", + subtext: cell?.defaultSubtext ?? def.defaultSubtext ?? "", + link: cell?.defaultLink ?? def.defaultLink ?? "", + imagePreview: resolveSlotImageUrl(defaultImage) ?? "", }); setModalOpen(true); } @@ -159,13 +171,16 @@ export default function WebsiteImagesPage() { setEditingId(item.id); setSlotSlug(def.slug); setTargetSortOrder(item.sortOrder); + const cell = def.cellLabels?.[item.sortOrder]; + const image = + item.imageUrl || cell?.defaultImage || def.defaultImage || ""; setForm({ nameFa: item.nameFa, nameEn: item.nameEn, subtext: item.subtext, linkText: item.linkText, link: item.link, - imagePreview: item.imageUrl ?? "", + imagePreview: resolveSlotImageUrl(image) ?? "", imageFile: null, }); setModalOpen(true); @@ -193,6 +208,17 @@ export default function WebsiteImagesPage() { const uploaded = await uploadMedia(form.imageFile, "website"); imageUrl = uploaded.url; imageKey = uploaded.storageKey; + } else { + const cell = + targetSortOrder !== undefined + ? activeDef?.cellLabels?.[targetSortOrder] + : undefined; + const fallback = + cell?.defaultImage ?? activeDef?.defaultImage ?? undefined; + // Keep storefront-relative defaults when saving without a new upload. + if (fallback && !editingId) { + imageUrl = fallback; + } } const payload = { @@ -299,7 +325,7 @@ export default function WebsiteImagesPage() { openEdit(def, item)} - onDelete={() => void handleDelete(item.id)} + onEdit={() => + item + ? openEdit(def, item) + : openCreate(def, index) + } + onDelete={ + item + ? () => void handleDelete(item.id) + : undefined + } /> ); } @@ -375,17 +413,31 @@ export default function WebsiteImagesPage() { // fixed const item = slotItems[0]; const sizeLabel = formatImageSize(def.aspectLabel); - if (item) { + const previewImage = resolveSlotImageUrl( + item?.imageUrl || def.defaultImage, + ); + if (item || previewImage) { return ( openEdit(def, item)} - onDelete={() => void handleDelete(item.id)} + onEdit={() => + item ? openEdit(def, item) : openCreate(def) + } + onDelete={ + item + ? () => void handleDelete(item.id) + : undefined + } /> ); } @@ -641,8 +693,13 @@ function SlotThumb({ title: string; subtitle?: string; onEdit: () => void; - onDelete: () => void; + onDelete?: () => void; }) { + const coverClass = + imageUrl?.includes("/images/transparent/") + ? "object-contain bg-[#ececec] p-2" + : `object-cover ${objectPosition ?? "object-center"}`; + return (
) : ( @@ -682,9 +739,11 @@ function SlotThumb({ - - - + {onDelete ? ( + + + + ) : null}
); diff --git a/src/data/website-slots.ts b/src/data/website-slots.ts index 075261e..9491688 100644 --- a/src/data/website-slots.ts +++ b/src/data/website-slots.ts @@ -17,6 +17,12 @@ export type WebsiteSlotDef = { * (preview only — uploaded file is full-bleed cover). */ previewObjectPosition?: string; + /** Storefront-relative default image when no upload exists yet */ + defaultImage?: string; + defaultNameFa?: string; + defaultNameEn?: string; + defaultSubtext?: string; + defaultLink?: string; cellLabels?: Array<{ labelFa: string; labelEn: string; @@ -25,6 +31,11 @@ export type WebsiteSlotDef = { /** Relative preview width in rem (matches on-site scale) */ previewWidthRem?: number; previewObjectPosition?: string; + defaultImage?: string; + defaultNameFa?: string; + defaultNameEn?: string; + defaultSubtext?: string; + defaultLink?: string; }>; }; @@ -65,52 +76,153 @@ 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 + * 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: "۱۶×۹", + previewObjectPosition: "object-[center_22%]", + 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", + previewObjectPosition: "object-[center_28%]", + defaultImage: + "/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg", + defaultNameFa: "مانتو", + defaultNameEn: "Mantel", + defaultSubtext: + "مانتوهایی با برش معاصر؛ ساده، شهری و مناسب هر روز.", + defaultLink: "/products?category=coat", + }, + { + labelFa: "پیراهن", + labelEn: "Shirts", + previewObjectPosition: "object-[center_35%]", + defaultImage: + "/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg", + defaultNameFa: "پیراهن", + defaultNameEn: "Shirts", + defaultSubtext: + "پیراهن‌هایی با پارچه لطیف و ایستایی تمیز برای استایل روزمره.", + defaultLink: "/products?category=shirts", + }, + { + labelFa: "شلوار", + labelEn: "Pants", + previewObjectPosition: "object-[center_30%]", + defaultImage: + "/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg", + defaultNameFa: "شلوار", + defaultNameEn: "Pants", + defaultSubtext: + "شلوارهایی با فرم خوش‌ایست که شکل خود را حفظ می‌کنند.", + defaultLink: "/products?category=pants", + }, + { + labelFa: "کیف زنانه", + labelEn: "Bags", + previewObjectPosition: "object-[center_40%]", + defaultImage: + "/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg", + defaultNameFa: "کیف زنانه", + defaultNameEn: "Bags", + defaultSubtext: + "کیف‌هایی با جزئیات دقیق؛ تکمیل‌کننده استایل مانا.", + defaultLink: "/products?category=bags", }, ], }, @@ -154,7 +266,12 @@ export const WEBSITE_SLOT_DEFS: WebsiteSlotDef[] = [ kind: "fixed", aspectRatio: 1920 / 585, aspectLabel: "1920×585", - previewObjectPosition: "object-left-bottom", + // Storefront crops mobile to the subject on the left + previewObjectPosition: "object-[22%_center]", + defaultImage: "/images/home/banners/leather.png", + defaultNameFa: "هنر پوشیدن", + defaultNameEn: "THE ART OF DRESSING", + defaultLink: "/products?collection=leather", }, { slug: "four-portrait-banner", @@ -239,15 +356,39 @@ export function normalizeSlotDef(raw: WebsiteSlotDefApi): WebsiteSlotDef { aspectLabel: local?.aspectLabel || raw.aspectLabel || "", previewObjectPosition: raw.previewObjectPosition ?? local?.previewObjectPosition, - cellLabels: (local?.cellLabels ?? raw.cellLabels)?.map((cell, index) => ({ - ...cell, - previewObjectPosition: - cell.previewObjectPosition ?? - local?.cellLabels?.[index]?.previewObjectPosition, - })), + defaultImage: raw.defaultImage ?? local?.defaultImage, + defaultNameFa: raw.defaultNameFa ?? local?.defaultNameFa, + defaultNameEn: raw.defaultNameEn ?? local?.defaultNameEn, + defaultSubtext: raw.defaultSubtext ?? local?.defaultSubtext, + defaultLink: raw.defaultLink ?? local?.defaultLink, + cellLabels: (local?.cellLabels ?? raw.cellLabels)?.map((cell, index) => { + const localCell = local?.cellLabels?.[index]; + return { + ...cell, + previewObjectPosition: + cell.previewObjectPosition ?? localCell?.previewObjectPosition, + defaultImage: cell.defaultImage ?? localCell?.defaultImage, + defaultNameFa: cell.defaultNameFa ?? localCell?.defaultNameFa, + defaultNameEn: cell.defaultNameEn ?? localCell?.defaultNameEn, + defaultSubtext: cell.defaultSubtext ?? localCell?.defaultSubtext, + defaultLink: cell.defaultLink ?? localCell?.defaultLink, + }; + }), }; } +/** Resolve storefront-relative paths for dashboard previews. */ +export function resolveSlotImageUrl(imageUrl?: string | null) { + if (!imageUrl) return null; + if (/^https?:\/\//i.test(imageUrl) || imageUrl.startsWith("blob:")) { + return imageUrl; + } + const site = + process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") || + "http://localhost:3000"; + return `${site}${imageUrl.startsWith("/") ? "" : "/"}${imageUrl}`; +} + /** Same focal points as website `map-home-slots` hero slides */ export function heroPreviewObjectPosition(index: number) { if (index === 0) return "object-[center_20%]";