Initial commit: Mana Collection dashboards (Next.js).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
export type BlogPost = {
|
||||
id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
author: string;
|
||||
abstract: string;
|
||||
body: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
metaKeywords: string;
|
||||
};
|
||||
|
||||
export const blogPosts: BlogPost[] = [
|
||||
{
|
||||
id: "bl1",
|
||||
title: "زیبایی در سادگی",
|
||||
date: "2026-03-12",
|
||||
author: "سارا محمدی",
|
||||
abstract: "نگاهی به مینیمالیسم در طراحی لباس و نقش خطوط آرام در کالکشن مانا.",
|
||||
body: "<p>سادگی فقط حذف نیست؛ انتخاب آگاهانه است. در این مطلب درباره بافت، رنگ و فرم صحبت میکنیم.</p>",
|
||||
image: "/images/ahmad-ebadi-r5IzkiO08JU-unsplash.jpg",
|
||||
tags: ["مینیمال", "طراحی"],
|
||||
metaTitle: "زیبایی در سادگی | مانا",
|
||||
metaDescription: "نگاهی به مینیمالیسم در طراحی لباس",
|
||||
metaKeywords: "مینیمال، لباس، مانا",
|
||||
},
|
||||
{
|
||||
id: "bl2",
|
||||
title: "پارچههای فصل بهار",
|
||||
date: "2026-02-28",
|
||||
author: "نیلوفر احمدی",
|
||||
abstract: "انتخاب پارچه مناسب برای هوای ملایم بهار و ترکیب رنگهای خنثی.",
|
||||
body: "<p>بهار فرصت بازی با بافتهای سبک است. لینن، پنبه نرم و لایههای شفاف.</p>",
|
||||
image: "/images/jonathan-borba-XJt51hAa3z8-unsplash.jpg",
|
||||
tags: ["بهار", "پارچه"],
|
||||
metaTitle: "پارچههای فصل بهار | مانا",
|
||||
metaDescription: "راهنمای انتخاب پارچه برای بهار",
|
||||
metaKeywords: "بهار، پارچه، کالکشن",
|
||||
},
|
||||
{
|
||||
id: "bl3",
|
||||
title: "استایل روزمره شیک",
|
||||
date: "2026-01-18",
|
||||
author: "مریم کریمی",
|
||||
abstract: "چگونه با چند قطعه پایه، استایلی مرتب و ماندگار بسازید.",
|
||||
body: "<p>قطعات پایه مثل مانتو کلاسیک و پیراهن ساده، ستون استایل روزمرهاند.</p>",
|
||||
image: "/images/aiony-haust-owp8uQgoK8U-unsplash.jpg",
|
||||
tags: ["استایل", "روزمره"],
|
||||
metaTitle: "استایل روزمره شیک | مانا",
|
||||
metaDescription: "ساخت استایل روزمره با قطعات پایه",
|
||||
metaKeywords: "استایل، روزمره",
|
||||
},
|
||||
{
|
||||
id: "bl4",
|
||||
title: "رنگهای خنثی در کمد",
|
||||
date: "2025-12-05",
|
||||
author: "الهام رضایی",
|
||||
abstract: "چرا پالت مشکی، سفید و بژ همیشه کار میکند و چگونه ترکیب شود.",
|
||||
body: "<p>رنگهای خنثی فضای تنفس به استایل میدهند و ترکیب آنها آسان است.</p>",
|
||||
image: "/images/ola-szkolda-J61EvbE1UCE-unsplash.jpg",
|
||||
tags: ["رنگ", "کمد"],
|
||||
metaTitle: "رنگهای خنثی در کمد | مانا",
|
||||
metaDescription: "کار با پالت خنثی در استایل",
|
||||
metaKeywords: "رنگ، مشکی، بژ",
|
||||
},
|
||||
];
|
||||
|
||||
export function formatBlogDate(isoDate: string) {
|
||||
const date = new Date(`${isoDate}T12:00:00`);
|
||||
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
export type Brand = {
|
||||
id: string;
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type CategoryVariants = {
|
||||
colors: string[];
|
||||
sizes: string[];
|
||||
packages: Array<"6" | "12">;
|
||||
};
|
||||
|
||||
export type CategoryNode = {
|
||||
id: string;
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
children?: CategoryNode[];
|
||||
variants?: CategoryVariants;
|
||||
};
|
||||
|
||||
export type ProductVariantPrice = {
|
||||
id: string;
|
||||
colorId: string;
|
||||
sizeId: string;
|
||||
packageId: string;
|
||||
price: string;
|
||||
discountPrice?: string;
|
||||
};
|
||||
|
||||
export type Product = {
|
||||
id: string;
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
summary?: string;
|
||||
description?: string;
|
||||
categoryId: string;
|
||||
brandId: string;
|
||||
price: number;
|
||||
discountPrice?: number;
|
||||
image: string;
|
||||
gallery?: string[];
|
||||
tags: string[];
|
||||
variantPrices?: ProductVariantPrice[];
|
||||
};
|
||||
|
||||
/** Mock DB catalogs for category variants */
|
||||
export const colorCatalog = [
|
||||
{ id: "black", nameFa: "مشکی", hex: "#0a0a0a" },
|
||||
{ id: "white", nameFa: "سفید", hex: "#f5f5f5" },
|
||||
{ id: "cream", nameFa: "کرم", hex: "#e8dcc8" },
|
||||
{ id: "beige", nameFa: "بژ", hex: "#c4a882" },
|
||||
{ id: "navy", nameFa: "سرمهای", hex: "#1b2a4a" },
|
||||
{ id: "olive", nameFa: "زیتونی", hex: "#556b2f" },
|
||||
{ id: "burgundy", nameFa: "زرشکی", hex: "#6b1e2a" },
|
||||
{ id: "gray", nameFa: "طوسی", hex: "#8a8a8a" },
|
||||
{ id: "brown", nameFa: "قهوهای", hex: "#6b4423" },
|
||||
{ id: "red", nameFa: "قرمز", hex: "#b91c1c" },
|
||||
];
|
||||
|
||||
export const sizeCatalog = [
|
||||
{ id: "xs", nameFa: "XS" },
|
||||
{ id: "s", nameFa: "S" },
|
||||
{ id: "m", nameFa: "M" },
|
||||
{ id: "l", nameFa: "L" },
|
||||
{ id: "xl", nameFa: "XL" },
|
||||
{ id: "xxl", nameFa: "XXL" },
|
||||
{ id: "36", nameFa: "36" },
|
||||
{ id: "38", nameFa: "38" },
|
||||
{ id: "40", nameFa: "40" },
|
||||
{ id: "42", nameFa: "42" },
|
||||
{ id: "44", nameFa: "44" },
|
||||
];
|
||||
|
||||
export const packageCatalog: Array<{ id: "6" | "12"; nameFa: string }> = [
|
||||
{ id: "6", nameFa: "۶ تایی" },
|
||||
{ id: "12", nameFa: "۱۲ تایی" },
|
||||
];
|
||||
|
||||
export const emptyVariants = (): CategoryVariants => ({
|
||||
colors: [],
|
||||
sizes: [],
|
||||
packages: [],
|
||||
});
|
||||
|
||||
export const brands: Brand[] = [
|
||||
{
|
||||
id: "b1",
|
||||
nameFa: "مانا",
|
||||
nameEn: "Mana",
|
||||
image: "/images/ola-szkolda-J61EvbE1UCE-unsplash.jpg",
|
||||
},
|
||||
{
|
||||
id: "b2",
|
||||
nameFa: "نویر",
|
||||
nameEn: "Noir",
|
||||
image: "/images/aiony-haust-owp8uQgoK8U-unsplash.jpg",
|
||||
},
|
||||
{
|
||||
id: "b3",
|
||||
nameFa: "لومن",
|
||||
nameEn: "Lumen",
|
||||
image: "/images/jonathan-borba-XJt51hAa3z8-unsplash.jpg",
|
||||
},
|
||||
{
|
||||
id: "b4",
|
||||
nameFa: "اتلیه",
|
||||
nameEn: "Atelier",
|
||||
image: "/images/ahmad-ebadi-r5IzkiO08JU-unsplash.jpg",
|
||||
},
|
||||
];
|
||||
|
||||
export const categories: CategoryNode[] = [
|
||||
{
|
||||
id: "c1",
|
||||
nameFa: "زنانه",
|
||||
nameEn: "Women",
|
||||
children: [
|
||||
{
|
||||
id: "c1-1",
|
||||
nameFa: "مانتو",
|
||||
nameEn: "Manteau",
|
||||
variants: {
|
||||
colors: ["black", "cream", "navy", "gray"],
|
||||
sizes: ["s", "m", "l", "xl"],
|
||||
packages: ["6"],
|
||||
},
|
||||
children: [
|
||||
{ id: "c1-1-1", nameFa: "بهاره", nameEn: "Spring" },
|
||||
{ id: "c1-1-2", nameFa: "پاییزه", nameEn: "Autumn" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c1-2",
|
||||
nameFa: "پیراهن",
|
||||
nameEn: "Dress",
|
||||
variants: {
|
||||
colors: ["white", "olive", "red", "beige"],
|
||||
sizes: ["36", "38", "40", "42"],
|
||||
packages: ["6", "12"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "c1-3",
|
||||
nameFa: "شلوار",
|
||||
nameEn: "Trousers",
|
||||
variants: {
|
||||
colors: ["black", "beige", "olive"],
|
||||
sizes: ["38", "40", "42", "44"],
|
||||
packages: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
nameFa: "مردانه",
|
||||
nameEn: "Men",
|
||||
children: [
|
||||
{ id: "c2-1", nameFa: "کت", nameEn: "Jacket" },
|
||||
{ id: "c2-2", nameFa: "پیراهن رسمی", nameEn: "Shirt" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "c3",
|
||||
nameFa: "اکسسوری",
|
||||
nameEn: "Accessories",
|
||||
children: [
|
||||
{ id: "c3-1", nameFa: "کیف", nameEn: "Bag" },
|
||||
{ id: "c3-2", nameFa: "کفش", nameEn: "Shoes" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
id: "p1",
|
||||
nameFa: "مانتو مشکی کلاسیک",
|
||||
nameEn: "Classic Black Manteau",
|
||||
categoryId: "c1-1",
|
||||
brandId: "b1",
|
||||
price: 4200000,
|
||||
discountPrice: 3570000,
|
||||
image: "/images/aiony-haust-owp8uQgoK8U-unsplash.jpg",
|
||||
tags: ["کلاسیک", "مشکی"],
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
nameFa: "پیراهن سفید مینیمال",
|
||||
nameEn: "Minimal White Dress",
|
||||
categoryId: "c1-2",
|
||||
brandId: "b3",
|
||||
price: 2800000,
|
||||
image: "/images/jonathan-borba-XJt51hAa3z8-unsplash.jpg",
|
||||
tags: ["مینیمال", "سفید"],
|
||||
},
|
||||
{
|
||||
id: "p3",
|
||||
nameFa: "کت پشمی طوسی",
|
||||
nameEn: "Grey Wool Coat",
|
||||
categoryId: "c2-1",
|
||||
brandId: "b2",
|
||||
price: 5100000,
|
||||
discountPrice: 4590000,
|
||||
image: "/images/ola-szkolda-J61EvbE1UCE-unsplash.jpg",
|
||||
tags: ["زمستانه"],
|
||||
},
|
||||
{
|
||||
id: "p4",
|
||||
nameFa: "شلوار بگ کرم",
|
||||
nameEn: "Cream Baggy Pants",
|
||||
categoryId: "c1-3",
|
||||
brandId: "b1",
|
||||
price: 1950000,
|
||||
image: "/images/chyntia-juls-HlVjI5WmoQY-unsplash.jpg",
|
||||
tags: ["روزمره"],
|
||||
},
|
||||
{
|
||||
id: "p5",
|
||||
nameFa: "بلوز ابریشمی زیتونی",
|
||||
nameEn: "Olive Silk Blouse",
|
||||
categoryId: "c1-2",
|
||||
brandId: "b4",
|
||||
price: 2400000,
|
||||
discountPrice: 1920000,
|
||||
image: "/images/valerie-elash-T5tIoXFJfSk-unsplash.jpg",
|
||||
tags: ["ابریشم"],
|
||||
},
|
||||
{
|
||||
id: "p6",
|
||||
nameFa: "دامن پلیسه مشکی",
|
||||
nameEn: "Black Pleated Skirt",
|
||||
categoryId: "c1-2",
|
||||
brandId: "b2",
|
||||
price: 1680000,
|
||||
image: "/images/eve-maier-u1OuYQa0WtQ-unsplash.jpg",
|
||||
tags: ["مجلسی"],
|
||||
},
|
||||
{
|
||||
id: "p7",
|
||||
nameFa: "کت جین روشن",
|
||||
nameEn: "Light Denim Jacket",
|
||||
categoryId: "c2-1",
|
||||
brandId: "b3",
|
||||
price: 3200000,
|
||||
discountPrice: 2880000,
|
||||
image: "/images/amir-asghari-xoEb3kS_YoM-unsplash.jpg",
|
||||
tags: ["جین"],
|
||||
},
|
||||
{
|
||||
id: "p8",
|
||||
nameFa: "پیراهن زرد ابریشمی",
|
||||
nameEn: "Yellow Silk Dress",
|
||||
categoryId: "c1-2",
|
||||
brandId: "b4",
|
||||
price: 3900000,
|
||||
image: "/images/ryanwar-hanif-98XENwtPMCc-unsplash.jpg",
|
||||
tags: ["رنگارنگ"],
|
||||
},
|
||||
];
|
||||
|
||||
export function formatPrice(value: number) {
|
||||
return new Intl.NumberFormat("en-US").format(value);
|
||||
}
|
||||
|
||||
export function discountPercent(price: number, discountPrice: number) {
|
||||
return Math.round(((price - discountPrice) / price) * 100);
|
||||
}
|
||||
|
||||
export function flattenCategories(
|
||||
nodes: CategoryNode[],
|
||||
prefix = "",
|
||||
): { id: string; label: string }[] {
|
||||
return nodes.flatMap((node) => {
|
||||
const label = prefix ? `${prefix} / ${node.nameFa}` : node.nameFa;
|
||||
return [
|
||||
{ id: node.id, label },
|
||||
...(node.children ? flattenCategories(node.children, label) : []),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export function findCategoryNode(
|
||||
nodes: CategoryNode[],
|
||||
id: string,
|
||||
): CategoryNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
if (node.children?.length) {
|
||||
const found = findCategoryNode(node.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolve variants from category or nearest parent that has them */
|
||||
export function resolveCategoryVariants(
|
||||
categoryId: string,
|
||||
nodes: CategoryNode[] = categories,
|
||||
): CategoryVariants | null {
|
||||
let current = findCategoryNode(nodes, categoryId);
|
||||
while (current) {
|
||||
if (
|
||||
current.variants &&
|
||||
(current.variants.colors.length ||
|
||||
current.variants.sizes.length ||
|
||||
current.variants.packages.length)
|
||||
) {
|
||||
return current.variants;
|
||||
}
|
||||
const parent = findParentCategory(nodes, current.id);
|
||||
if (!parent) break;
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findParentCategory(
|
||||
nodes: CategoryNode[],
|
||||
childId: string,
|
||||
): CategoryNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.children?.some((c) => c.id === childId)) return node;
|
||||
if (node.children?.length) {
|
||||
const found = findParentCategory(node.children, childId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getCategoryVariantOptions(
|
||||
categoryId: string,
|
||||
nodes: CategoryNode[] = categories,
|
||||
) {
|
||||
const groups = getCategoryVariantGroups(categoryId, nodes);
|
||||
return [
|
||||
...groups.colors.map((opt) => ({
|
||||
value: `color:${opt.value}`,
|
||||
label: `رنگ · ${opt.label}`,
|
||||
})),
|
||||
...groups.sizes.map((opt) => ({
|
||||
value: `size:${opt.value}`,
|
||||
label: `سایز · ${opt.label}`,
|
||||
})),
|
||||
...groups.packages.map((opt) => ({
|
||||
value: `package:${opt.value}`,
|
||||
label: `پکیج · ${opt.label}`,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
export function getCategoryVariantGroups(
|
||||
categoryId: string,
|
||||
nodes: CategoryNode[] = categories,
|
||||
) {
|
||||
const variants = resolveCategoryVariants(categoryId, nodes);
|
||||
const empty = {
|
||||
colors: [] as { value: string; label: string }[],
|
||||
sizes: [] as { value: string; label: string }[],
|
||||
packages: [] as { value: string; label: string }[],
|
||||
};
|
||||
if (!variants) return empty;
|
||||
|
||||
return {
|
||||
colors: variants.colors
|
||||
.map((id) => {
|
||||
const color = colorCatalog.find((item) => item.id === id);
|
||||
return color ? { value: id, label: color.nameFa } : null;
|
||||
})
|
||||
.filter(Boolean) as { value: string; label: string }[],
|
||||
sizes: variants.sizes
|
||||
.map((id) => {
|
||||
const size = sizeCatalog.find((item) => item.id === id);
|
||||
return size ? { value: id, label: size.nameFa } : null;
|
||||
})
|
||||
.filter(Boolean) as { value: string; label: string }[],
|
||||
packages: variants.packages
|
||||
.map((id) => {
|
||||
const pack = packageCatalog.find((item) => item.id === id);
|
||||
return pack
|
||||
? { value: id, label: `${pack.nameFa} (${id} pcs)` }
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean) as { value: string; label: string }[],
|
||||
};
|
||||
}
|
||||
|
||||
export function getVariantLabel(
|
||||
row: Pick<ProductVariantPrice, "colorId" | "sizeId" | "packageId">,
|
||||
categoryId: string,
|
||||
nodes: CategoryNode[] = categories,
|
||||
) {
|
||||
const groups = getCategoryVariantGroups(categoryId, nodes);
|
||||
const parts: string[] = [];
|
||||
if (row.colorId) {
|
||||
const color = groups.colors.find((opt) => opt.value === row.colorId);
|
||||
if (color) parts.push(`رنگ · ${color.label}`);
|
||||
}
|
||||
if (row.sizeId) {
|
||||
const size = groups.sizes.find((opt) => opt.value === row.sizeId);
|
||||
if (size) parts.push(`سایز · ${size.label}`);
|
||||
}
|
||||
if (row.packageId) {
|
||||
const pack = groups.packages.find((opt) => opt.value === row.packageId);
|
||||
if (pack) parts.push(`پکیج · ${pack.label}`);
|
||||
}
|
||||
return parts.length ? parts.join(" / ") : "بدون تنوع";
|
||||
}
|
||||
|
||||
export function findCategoryPath(
|
||||
nodes: CategoryNode[],
|
||||
id: string,
|
||||
trail: string[] = [],
|
||||
): string[] | null {
|
||||
for (const node of nodes) {
|
||||
const next = [...trail, node.nameFa];
|
||||
if (node.id === id) return next;
|
||||
if (node.children?.length) {
|
||||
const found = findCategoryPath(node.children, id, next);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function categoryMatchesQuery(node: CategoryNode, query: string): boolean {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
const self =
|
||||
node.nameFa.includes(query.trim()) ||
|
||||
node.nameEn.toLowerCase().includes(q);
|
||||
if (self) return true;
|
||||
return Boolean(node.children?.some((child) => categoryMatchesQuery(child, query)));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export type SubscriptionStatus = "regular" | "premium";
|
||||
|
||||
export type Customer = {
|
||||
id: string;
|
||||
nameFa: string;
|
||||
phone: string;
|
||||
joinedAt: string;
|
||||
subscription: SubscriptionStatus;
|
||||
};
|
||||
|
||||
export const customers: Customer[] = [
|
||||
{
|
||||
id: "cu1",
|
||||
nameFa: "سارا محمدی",
|
||||
phone: "09121234567",
|
||||
joinedAt: "2025-11-12",
|
||||
subscription: "premium",
|
||||
},
|
||||
{
|
||||
id: "cu2",
|
||||
nameFa: "نیلوفر احمدی",
|
||||
phone: "09351239876",
|
||||
joinedAt: "2026-01-04",
|
||||
subscription: "regular",
|
||||
},
|
||||
{
|
||||
id: "cu3",
|
||||
nameFa: "مریم کریمی",
|
||||
phone: "09197654321",
|
||||
joinedAt: "2025-08-21",
|
||||
subscription: "premium",
|
||||
},
|
||||
{
|
||||
id: "cu4",
|
||||
nameFa: "الهام رضایی",
|
||||
phone: "09014567890",
|
||||
joinedAt: "2026-02-18",
|
||||
subscription: "regular",
|
||||
},
|
||||
{
|
||||
id: "cu5",
|
||||
nameFa: "هانیه موسوی",
|
||||
phone: "09128887766",
|
||||
joinedAt: "2025-12-02",
|
||||
subscription: "regular",
|
||||
},
|
||||
{
|
||||
id: "cu6",
|
||||
nameFa: "یاسمن نوری",
|
||||
phone: "09361112233",
|
||||
joinedAt: "2026-03-09",
|
||||
subscription: "premium",
|
||||
},
|
||||
{
|
||||
id: "cu7",
|
||||
nameFa: "زهرا حسینی",
|
||||
phone: "09201230000",
|
||||
joinedAt: "2025-06-30",
|
||||
subscription: "regular",
|
||||
},
|
||||
{
|
||||
id: "cu8",
|
||||
nameFa: "فرناز اکبری",
|
||||
phone: "09193456789",
|
||||
joinedAt: "2026-04-01",
|
||||
subscription: "premium",
|
||||
},
|
||||
];
|
||||
|
||||
export function formatJoinedDate(isoDate: string) {
|
||||
const date = new Date(`${isoDate}T12:00:00`);
|
||||
return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function subscriptionLabel(status: SubscriptionStatus) {
|
||||
return status === "premium" ? "پرمیوم" : "عادی";
|
||||
}
|
||||
Reference in New Issue
Block a user