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` }}
>
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%]";