Autosave: 20260328-015333
This commit is contained in:
parent
3d23e78133
commit
de5aa451c1
@ -147,7 +147,7 @@ router.get("/integrations/shein-categories", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
let categories: SheinCategory[] = [];
|
let categories: SheinCategory[] = [];
|
||||||
let source = "preset";
|
let source = "preset";
|
||||||
let scrapeResult: { success: boolean; error?: string; runId?: string } | null = null;
|
let scrapeResult: Awaited<ReturnType<typeof fetchSheinCategories>> | null = null;
|
||||||
|
|
||||||
if (mode === "scrape") {
|
if (mode === "scrape") {
|
||||||
scrapeResult = await fetchSheinCategories();
|
scrapeResult = await fetchSheinCategories();
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import { requireAdmin } from "../middleware/auth";
|
|||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
|
function getSingleParamValue(value: string | string[] | undefined): string {
|
||||||
|
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
function generateOrderNumber(): string {
|
function generateOrderNumber(): string {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const random = Math.floor(Math.random() * 1000).toString().padStart(3, "0");
|
const random = Math.floor(Math.random() * 1000).toString().padStart(3, "0");
|
||||||
@ -41,7 +45,7 @@ router.get("/orders", async (req, res) => {
|
|||||||
|
|
||||||
router.get("/orders/:id", async (req, res) => {
|
router.get("/orders/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(getSingleParamValue(req.params.id), 10);
|
||||||
const [order] = await db.select().from(ordersTable).where(eq(ordersTable.id, id));
|
const [order] = await db.select().from(ordersTable).where(eq(ordersTable.id, id));
|
||||||
if (!order) return res.status(404).json({ error: "Order not found" });
|
if (!order) return res.status(404).json({ error: "Order not found" });
|
||||||
res.json(order);
|
res.json(order);
|
||||||
@ -191,7 +195,7 @@ router.post("/orders", async (req, res) => {
|
|||||||
|
|
||||||
router.delete("/orders/:id", requireAdmin, async (req, res) => {
|
router.delete("/orders/:id", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(getSingleParamValue(req.params.id), 10);
|
||||||
await db.delete(ordersTable).where(eq(ordersTable.id, id));
|
await db.delete(ordersTable).where(eq(ordersTable.id, id));
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -202,7 +206,7 @@ router.delete("/orders/:id", requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
router.put("/orders/:id/status", async (req, res) => {
|
router.put("/orders/:id/status", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(getSingleParamValue(req.params.id), 10);
|
||||||
const { status, tracking_number } = req.body;
|
const { status, tracking_number } = req.body;
|
||||||
|
|
||||||
// Fetch current order first
|
// Fetch current order first
|
||||||
|
|||||||
@ -3,9 +3,14 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"types": ["node"]
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"noImplicitReturns": false
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../lib/db"
|
"path": "../../lib/db"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1470
artifacts/extra-store/src/lib/admin-preview-api.ts
Normal file
1470
artifacts/extra-store/src/lib/admin-preview-api.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -209,6 +209,7 @@ export const translations = {
|
|||||||
section_trending_title: "الأكثر رواجاً",
|
section_trending_title: "الأكثر رواجاً",
|
||||||
section_bestseller_title: "الأكثر مبيعاً",
|
section_bestseller_title: "الأكثر مبيعاً",
|
||||||
section_new_title: "وصل حديثاً",
|
section_new_title: "وصل حديثاً",
|
||||||
|
section_top_rated_title: "أعلى تقييماً",
|
||||||
shein_section_title: "أزياء، جمال ومنزل",
|
shein_section_title: "أزياء، جمال ومنزل",
|
||||||
browse_all_cat: "تصفح جميع المنتجات",
|
browse_all_cat: "تصفح جميع المنتجات",
|
||||||
// Mega menu
|
// Mega menu
|
||||||
@ -460,6 +461,7 @@ export const translations = {
|
|||||||
section_trending_title: "Trending",
|
section_trending_title: "Trending",
|
||||||
section_bestseller_title: "Best Sellers",
|
section_bestseller_title: "Best Sellers",
|
||||||
section_new_title: "New Arrivals",
|
section_new_title: "New Arrivals",
|
||||||
|
section_top_rated_title: "Top Rated",
|
||||||
shein_section_title: "Fashion, Beauty & Home",
|
shein_section_title: "Fashion, Beauty & Home",
|
||||||
browse_all_cat: "Browse All Products",
|
browse_all_cat: "Browse All Products",
|
||||||
// Mega menu
|
// Mega menu
|
||||||
|
|||||||
118
artifacts/extra-store/src/lib/mock-auth.ts
Normal file
118
artifacts/extra-store/src/lib/mock-auth.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
export type PreviewAuthUser = {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StoredPreviewUser = PreviewAuthUser & {
|
||||||
|
password: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORE_USERS_KEY = "extra_preview_users";
|
||||||
|
const STORE_AUTH_SALT = "extra_preview_auth_v1";
|
||||||
|
export const PREVIEW_ADMIN_TOKEN = "preview_admin_token";
|
||||||
|
|
||||||
|
const DEMO_PREVIEW_USER: StoredPreviewUser = {
|
||||||
|
id: 1,
|
||||||
|
name: "عميل تجريبي",
|
||||||
|
email: "demo@extra.sa",
|
||||||
|
password: "Extra123",
|
||||||
|
created_at: "2026-03-28T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
function readUsers(): StoredPreviewUser[] {
|
||||||
|
if (typeof localStorage === "undefined") return [DEMO_PREVIEW_USER];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(localStorage.getItem(STORE_USERS_KEY) || "[]");
|
||||||
|
const users = Array.isArray(parsed) ? parsed : [];
|
||||||
|
return users.some((user) => user.email === DEMO_PREVIEW_USER.email) ? users : [DEMO_PREVIEW_USER, ...users];
|
||||||
|
} catch {
|
||||||
|
return [DEMO_PREVIEW_USER];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeUsers(users: StoredPreviewUser[]) {
|
||||||
|
if (typeof localStorage === "undefined") return;
|
||||||
|
localStorage.setItem(STORE_USERS_KEY, JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEmail(email: string) {
|
||||||
|
return email.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeToken(userId: number) {
|
||||||
|
return `preview_user_${userId}_${STORE_AUTH_SALT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJsonResponse(res: Response) {
|
||||||
|
const contentType = res.headers.get("content-type") || "";
|
||||||
|
return contentType.includes("application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPreviewStoreUser(input: {
|
||||||
|
name?: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirm_password?: string;
|
||||||
|
}) {
|
||||||
|
const email = normalizeEmail(input.email);
|
||||||
|
const name = input.name?.trim() || null;
|
||||||
|
const password = input.password || "";
|
||||||
|
const confirm = input.confirm_password || "";
|
||||||
|
|
||||||
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
throw new Error("البريد الإلكتروني غير صحيح");
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
throw new Error("كلمة المرور يجب أن تكون 8 أحرف على الأقل");
|
||||||
|
}
|
||||||
|
if (!/[A-Z]/.test(password)) {
|
||||||
|
throw new Error("كلمة المرور يجب أن تحتوي على حرف كبير");
|
||||||
|
}
|
||||||
|
if (!/[0-9]/.test(password)) {
|
||||||
|
throw new Error("كلمة المرور يجب أن تحتوي على رقم");
|
||||||
|
}
|
||||||
|
if (password !== confirm) {
|
||||||
|
throw new Error("كلمة المرور وتأكيدها غير متطابقين");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = readUsers();
|
||||||
|
if (users.some((user) => user.email === email)) {
|
||||||
|
throw new Error("البريد الإلكتروني مستخدم بالفعل");
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = users.reduce((max, user) => Math.max(max, user.id), 0) + 1;
|
||||||
|
const newUser: StoredPreviewUser = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
users.push(newUser);
|
||||||
|
writeUsers(users);
|
||||||
|
|
||||||
|
const user: PreviewAuthUser = { id: newUser.id, name: newUser.name, email: newUser.email };
|
||||||
|
return { user, token: makeToken(user.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginPreviewStoreUser(input: { email: string; password: string }) {
|
||||||
|
const email = normalizeEmail(input.email);
|
||||||
|
const users = readUsers();
|
||||||
|
const user = users.find((entry) => entry.email === email && entry.password === input.password);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("البريد الإلكتروني أو كلمة المرور غير صحيحة");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
user: { id: user.id, name: user.name, email: user.email },
|
||||||
|
token: makeToken(user.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginPreviewAdmin(input: { username: string; password: string }) {
|
||||||
|
if (input.username === "admin" && input.password === "admin123") {
|
||||||
|
return { token: PREVIEW_ADMIN_TOKEN, username: "admin" };
|
||||||
|
}
|
||||||
|
throw new Error("بيانات الدخول غير صحيحة");
|
||||||
|
}
|
||||||
980
artifacts/extra-store/src/lib/store-fallback.ts
Normal file
980
artifacts/extra-store/src/lib/store-fallback.ts
Normal file
@ -0,0 +1,980 @@
|
|||||||
|
export type FallbackCategory = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_en: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
parent_id: number | null;
|
||||||
|
sort_order: number | null;
|
||||||
|
source: string | null;
|
||||||
|
slug: string | null;
|
||||||
|
shein_url: string | null;
|
||||||
|
image_url: string | null;
|
||||||
|
product_count?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FallbackCategoryNode = FallbackCategory & {
|
||||||
|
children: FallbackCategory[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FallbackProduct = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
name_en: string | null;
|
||||||
|
brand: string | null;
|
||||||
|
price: string;
|
||||||
|
original_price: string | null;
|
||||||
|
images: string[];
|
||||||
|
colors: string[];
|
||||||
|
sizes: string[];
|
||||||
|
specs: Record<string, string>;
|
||||||
|
marketing_points: string[];
|
||||||
|
subcategory: string | null;
|
||||||
|
category_id: number;
|
||||||
|
rating: string;
|
||||||
|
review_count: number;
|
||||||
|
stock: number;
|
||||||
|
is_trending: boolean;
|
||||||
|
is_bestseller: boolean;
|
||||||
|
is_new: boolean;
|
||||||
|
is_top_rated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FallbackProductsResponse = {
|
||||||
|
products: FallbackProduct[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
total_pages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXTRA_CATEGORY_SEEDS = [
|
||||||
|
[1, "الإلكترونيات", "Electronics", "📱", "electronics"],
|
||||||
|
[2, "الموضة والأزياء", "Fashion & Apparel", "👗", "fashion"],
|
||||||
|
[3, "العناية الشخصية والجمال", "Beauty & Personal Care", "💄", "beauty"],
|
||||||
|
[4, "المنزل والمطبخ", "Home & Kitchen", "🏠", "home-kitchen"],
|
||||||
|
[5, "الأطفال والألعاب", "Kids & Toys", "🧸", "kids-toys"],
|
||||||
|
[6, "الرياضة واللياقة", "Sports & Fitness", "🏋️", "sports-fitness"],
|
||||||
|
[7, "السيارات", "Automotive", "🚗", "automotive"],
|
||||||
|
[8, "الكتب والأدوات المكتبية", "Books & Office", "📚", "books-office"],
|
||||||
|
[9, "الصحة والأدوية", "Health & Pharmacy", "🩺", "health-pharmacy"],
|
||||||
|
[10, "الحقائب والأحذية", "Bags & Shoes", "👜", "bags-shoes"],
|
||||||
|
[11, "المفروشات", "Furniture & Bedding", "🛋️", "furnishings"],
|
||||||
|
[12, "القرطاسية", "Stationery", "✏️", "stationery"],
|
||||||
|
[13, "الأجهزة الكهربائية", "Home Appliances", "🔌", "appliances"],
|
||||||
|
[
|
||||||
|
14,
|
||||||
|
"الجوالات وملحقاتها",
|
||||||
|
"Mobiles & Accessories",
|
||||||
|
"📲",
|
||||||
|
"mobiles-accessories",
|
||||||
|
],
|
||||||
|
[15, "الكمبيوتر وملحقاته", "Computers & Accessories", "💻", "computers"],
|
||||||
|
[16, "الكاميرات", "Cameras", "📷", "cameras"],
|
||||||
|
[17, "الألعاب الإلكترونية", "Gaming", "🎮", "gaming"],
|
||||||
|
[18, "مستلزمات الرحلات والتخييم", "Camping & Outdoor", "⛺", "camping"],
|
||||||
|
[19, "العطور والبخور", "Perfumes & Incense", "🪔", "perfumes"],
|
||||||
|
[20, "الهدايا", "Gifts", "🎁", "gifts"],
|
||||||
|
[
|
||||||
|
21,
|
||||||
|
"منتجات الأطفال (حفاضات، حليب)",
|
||||||
|
"Baby Essentials",
|
||||||
|
"🍼",
|
||||||
|
"baby-essentials",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
22,
|
||||||
|
"المواد الأساسية (منظفات، ورقيات)",
|
||||||
|
"Household Essentials",
|
||||||
|
"🧻",
|
||||||
|
"household-essentials",
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const EXTRA_SUBCATEGORY_MAP: Record<number, readonly [string, string][]> = {
|
||||||
|
1: [
|
||||||
|
["هواتف ذكية", "Smartphones"],
|
||||||
|
["شاشات ذكية", "Smart TVs"],
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
["عبايات", "Abayas"],
|
||||||
|
["فساتين", "Dresses"],
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
["عناية بالبشرة", "Skincare"],
|
||||||
|
["مكياج", "Makeup"],
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
["أدوات مطبخ", "Kitchen Tools"],
|
||||||
|
["ديكور منزلي", "Home Decor"],
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
["ألعاب تعليمية", "Educational Toys"],
|
||||||
|
["ملابس أطفال", "Kids Fashion"],
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
["أجهزة رياضية", "Fitness Equipment"],
|
||||||
|
["ملابس رياضية", "Sportswear"],
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
["إكسسوارات سيارة", "Car Accessories"],
|
||||||
|
["زيوت وصيانة", "Maintenance"],
|
||||||
|
],
|
||||||
|
8: [
|
||||||
|
["كتب عربية", "Arabic Books"],
|
||||||
|
["مستلزمات مكتب", "Office Supplies"],
|
||||||
|
],
|
||||||
|
9: [
|
||||||
|
["مكملات", "Supplements"],
|
||||||
|
["أجهزة صحية", "Health Devices"],
|
||||||
|
],
|
||||||
|
10: [
|
||||||
|
["حقائب نسائية", "Women's Bags"],
|
||||||
|
["أحذية رياضية", "Sneakers"],
|
||||||
|
],
|
||||||
|
11: [
|
||||||
|
["أرائك", "Sofas"],
|
||||||
|
["مفارش", "Bedding"],
|
||||||
|
],
|
||||||
|
12: [
|
||||||
|
["دفاتر", "Notebooks"],
|
||||||
|
["أقلام", "Pens"],
|
||||||
|
],
|
||||||
|
13: [
|
||||||
|
["غسالات", "Washers"],
|
||||||
|
["ثلاجات", "Refrigerators"],
|
||||||
|
],
|
||||||
|
14: [
|
||||||
|
["هواتف آيفون", "iPhone"],
|
||||||
|
["شواحن وكفرات", "Chargers & Cases"],
|
||||||
|
],
|
||||||
|
15: [
|
||||||
|
["لابتوبات", "Laptops"],
|
||||||
|
["شاشات مكتبية", "Monitors"],
|
||||||
|
],
|
||||||
|
16: [
|
||||||
|
["كاميرات فورية", "Instant Cameras"],
|
||||||
|
["عدسات", "Lenses"],
|
||||||
|
],
|
||||||
|
17: [
|
||||||
|
["أجهزة ألعاب", "Consoles"],
|
||||||
|
["إكسسوارات ألعاب", "Gaming Accessories"],
|
||||||
|
],
|
||||||
|
18: [
|
||||||
|
["خيام", "Tents"],
|
||||||
|
["حقائب رحلات", "Travel Gear"],
|
||||||
|
],
|
||||||
|
19: [
|
||||||
|
["عطور شرقية", "Oriental Perfumes"],
|
||||||
|
["بخور", "Incense"],
|
||||||
|
],
|
||||||
|
20: [
|
||||||
|
["هدايا فاخرة", "Luxury Gifts"],
|
||||||
|
["تغليف هدايا", "Gift Wrapping"],
|
||||||
|
],
|
||||||
|
21: [
|
||||||
|
["حفاضات", "Diapers"],
|
||||||
|
["حليب أطفال", "Infant Formula"],
|
||||||
|
],
|
||||||
|
22: [
|
||||||
|
["منظفات", "Cleaning Supplies"],
|
||||||
|
["ورقيات", "Paper Goods"],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXTRA_CATEGORIES: FallbackCategory[] = EXTRA_CATEGORY_SEEDS.map(
|
||||||
|
([id, name, name_en, icon, slug], index) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
name_en,
|
||||||
|
icon,
|
||||||
|
parent_id: null,
|
||||||
|
sort_order: index + 1,
|
||||||
|
source: "extra",
|
||||||
|
slug,
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
product_count: 4,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const EXTRA_TREE: FallbackCategoryNode[] = EXTRA_CATEGORIES.map((cat) => ({
|
||||||
|
...cat,
|
||||||
|
children: (EXTRA_SUBCATEGORY_MAP[cat.id] || []).map(([name, name_en], i) => ({
|
||||||
|
id: cat.id * 100 + i + 1,
|
||||||
|
name,
|
||||||
|
name_en,
|
||||||
|
icon: null,
|
||||||
|
parent_id: cat.id,
|
||||||
|
sort_order: i + 1,
|
||||||
|
source: "extra",
|
||||||
|
slug: `${cat.slug}-${i + 1}`,
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SHEIN_TREE: FallbackCategoryNode[] = [
|
||||||
|
{
|
||||||
|
id: 301,
|
||||||
|
name: "وصل حديثاً",
|
||||||
|
name_en: "New In",
|
||||||
|
icon: "✨",
|
||||||
|
parent_id: null,
|
||||||
|
sort_order: 100,
|
||||||
|
source: "shein",
|
||||||
|
slug: "new-in",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: "https://picsum.photos/seed/shein-new/600/900",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 30101,
|
||||||
|
name: "فساتين",
|
||||||
|
name_en: "Dresses",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 301,
|
||||||
|
sort_order: 1,
|
||||||
|
source: "shein",
|
||||||
|
slug: "new-in-dresses",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30102,
|
||||||
|
name: "بلايز",
|
||||||
|
name_en: "Tops",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 301,
|
||||||
|
sort_order: 2,
|
||||||
|
source: "shein",
|
||||||
|
slug: "new-in-tops",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 302,
|
||||||
|
name: "أزياء نسائية",
|
||||||
|
name_en: "Women Fashion",
|
||||||
|
icon: "👠",
|
||||||
|
parent_id: null,
|
||||||
|
sort_order: 101,
|
||||||
|
source: "shein",
|
||||||
|
slug: "women-fashion",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: "https://picsum.photos/seed/shein-women/600/900",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 30201,
|
||||||
|
name: "عبايات",
|
||||||
|
name_en: "Abayas",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 302,
|
||||||
|
sort_order: 1,
|
||||||
|
source: "shein",
|
||||||
|
slug: "women-fashion-abayas",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30202,
|
||||||
|
name: "ملابس سهرة",
|
||||||
|
name_en: "Evening Wear",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 302,
|
||||||
|
sort_order: 2,
|
||||||
|
source: "shein",
|
||||||
|
slug: "women-fashion-evening",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 303,
|
||||||
|
name: "الجمال",
|
||||||
|
name_en: "Beauty",
|
||||||
|
icon: "💅",
|
||||||
|
parent_id: null,
|
||||||
|
sort_order: 102,
|
||||||
|
source: "shein",
|
||||||
|
slug: "beauty",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: "https://picsum.photos/seed/shein-beauty/600/900",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 30301,
|
||||||
|
name: "مكياج",
|
||||||
|
name_en: "Makeup",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 303,
|
||||||
|
sort_order: 1,
|
||||||
|
source: "shein",
|
||||||
|
slug: "beauty-makeup",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30302,
|
||||||
|
name: "عناية",
|
||||||
|
name_en: "Care",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 303,
|
||||||
|
sort_order: 2,
|
||||||
|
source: "shein",
|
||||||
|
slug: "beauty-care",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 304,
|
||||||
|
name: "تخفيضات",
|
||||||
|
name_en: "Sale",
|
||||||
|
icon: "🔥",
|
||||||
|
parent_id: null,
|
||||||
|
sort_order: 103,
|
||||||
|
source: "shein",
|
||||||
|
slug: "sale",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: "https://picsum.photos/seed/shein-sale/600/900",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 30401,
|
||||||
|
name: "حتى 70%",
|
||||||
|
name_en: "Up to 70%",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 304,
|
||||||
|
sort_order: 1,
|
||||||
|
source: "shein",
|
||||||
|
slug: "sale-70",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30402,
|
||||||
|
name: "عروض اليوم",
|
||||||
|
name_en: "Today's Deals",
|
||||||
|
icon: null,
|
||||||
|
parent_id: 304,
|
||||||
|
sort_order: 2,
|
||||||
|
source: "shein",
|
||||||
|
slug: "sale-deals",
|
||||||
|
shein_url: null,
|
||||||
|
image_url: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FALLBACK_CATEGORY_TREE: FallbackCategoryNode[] = [
|
||||||
|
...EXTRA_TREE,
|
||||||
|
...SHEIN_TREE,
|
||||||
|
];
|
||||||
|
export const FALLBACK_CATEGORIES: FallbackCategory[] =
|
||||||
|
FALLBACK_CATEGORY_TREE.flatMap((node) => [
|
||||||
|
{ ...node, children: undefined as never },
|
||||||
|
...node.children,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PRODUCT_BLUEPRINTS = [
|
||||||
|
{
|
||||||
|
category_id: 1,
|
||||||
|
brand: "Samsung",
|
||||||
|
subcategory: "هواتف ذكية",
|
||||||
|
name: "Galaxy S25 Ultra أسود 512GB",
|
||||||
|
name_en: "Galaxy S25 Ultra Black 512GB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 1,
|
||||||
|
brand: "LG",
|
||||||
|
subcategory: "شاشات ذكية",
|
||||||
|
name: "شاشة OLED Evo مقاس 65 بوصة",
|
||||||
|
name_en: "OLED Evo TV 65-inch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 2,
|
||||||
|
brand: "Lamar",
|
||||||
|
subcategory: "عبايات",
|
||||||
|
name: "عباية سعودية فاخرة لون أسود",
|
||||||
|
name_en: "Luxury Saudi Abaya Black",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 2,
|
||||||
|
brand: "SHEIN",
|
||||||
|
subcategory: "فساتين",
|
||||||
|
name: "فستان سهرة نسائي كحلي مقاس L",
|
||||||
|
name_en: "Women's Evening Dress Navy L",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 3,
|
||||||
|
brand: "CeraVe",
|
||||||
|
subcategory: "عناية بالبشرة",
|
||||||
|
name: "كريم ترطيب يومي للبشرة الحساسة",
|
||||||
|
name_en: "Daily Moisturizing Cream",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 3,
|
||||||
|
brand: "Maybelline",
|
||||||
|
subcategory: "مكياج",
|
||||||
|
name: "أحمر شفاه ثابت لون روز",
|
||||||
|
name_en: "Longwear Lipstick Rose",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 4,
|
||||||
|
brand: "Tefal",
|
||||||
|
subcategory: "أدوات مطبخ",
|
||||||
|
name: "طقم قدور غير لاصق 10 قطع",
|
||||||
|
name_en: "10-Piece Non-Stick Cookware Set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 4,
|
||||||
|
brand: "Home Box",
|
||||||
|
subcategory: "ديكور منزلي",
|
||||||
|
name: "مرآة ديكور ذهبية لغرفة المعيشة",
|
||||||
|
name_en: "Golden Decor Mirror",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 5,
|
||||||
|
brand: "Lego",
|
||||||
|
subcategory: "ألعاب تعليمية",
|
||||||
|
name: "مكعبات تعليمية للأطفال +3",
|
||||||
|
name_en: "Educational Building Blocks 3+",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 5,
|
||||||
|
brand: "Disney",
|
||||||
|
subcategory: "ملابس أطفال",
|
||||||
|
name: "طقم أطفال قطن مريح لون سماوي",
|
||||||
|
name_en: "Kids Cotton Set Sky Blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 6,
|
||||||
|
brand: "Nike",
|
||||||
|
subcategory: "ملابس رياضية",
|
||||||
|
name: "طقم رياضي رجالي أسود مقاس XL",
|
||||||
|
name_en: "Men's Training Set Black XL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 6,
|
||||||
|
brand: "Xiaomi",
|
||||||
|
subcategory: "أجهزة رياضية",
|
||||||
|
name: "سوار ذكي لقياس النشاط اليومي",
|
||||||
|
name_en: "Smart Fitness Band",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 7,
|
||||||
|
brand: "Shell",
|
||||||
|
subcategory: "زيوت وصيانة",
|
||||||
|
name: "زيت محرك 5W-30 حماية عالية",
|
||||||
|
name_en: "Engine Oil 5W-30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 7,
|
||||||
|
brand: "Baseus",
|
||||||
|
subcategory: "إكسسوارات سيارة",
|
||||||
|
name: "حامل جوال مغناطيسي للسيارة",
|
||||||
|
name_en: "Magnetic Car Phone Holder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 8,
|
||||||
|
brand: "Jarir",
|
||||||
|
subcategory: "كتب عربية",
|
||||||
|
name: "رواية عربية الأكثر مبيعاً",
|
||||||
|
name_en: "Arabic Bestselling Novel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 8,
|
||||||
|
brand: "Deli",
|
||||||
|
subcategory: "مستلزمات مكتب",
|
||||||
|
name: "منظم مكتب عملي 6 قطع",
|
||||||
|
name_en: "Desk Organizer 6-Piece",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 9,
|
||||||
|
brand: "Centrum",
|
||||||
|
subcategory: "مكملات",
|
||||||
|
name: "فيتامينات متعددة للبالغين 60 كبسولة",
|
||||||
|
name_en: "Multivitamins 60 Capsules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 9,
|
||||||
|
brand: "Omron",
|
||||||
|
subcategory: "أجهزة صحية",
|
||||||
|
name: "جهاز قياس ضغط ذكي منزلي",
|
||||||
|
name_en: "Smart Blood Pressure Monitor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 10,
|
||||||
|
brand: "JW PEI",
|
||||||
|
subcategory: "حقائب نسائية",
|
||||||
|
name: "حقيبة كتف نسائية بنية فاخرة",
|
||||||
|
name_en: "Luxury Brown Shoulder Bag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 10,
|
||||||
|
brand: "Adidas",
|
||||||
|
subcategory: "أحذية رياضية",
|
||||||
|
name: "حذاء جري أبيض مقاس 42",
|
||||||
|
name_en: "Running Shoes White 42",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 11,
|
||||||
|
brand: "IKEA",
|
||||||
|
subcategory: "أرائك",
|
||||||
|
name: "كنبة زاوية رمادية 4 مقاعد",
|
||||||
|
name_en: "Grey Corner Sofa 4-Seater",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 11,
|
||||||
|
brand: "Sleep High",
|
||||||
|
subcategory: "مفارش",
|
||||||
|
name: "طقم مفرش سرير فندقي مزدوج",
|
||||||
|
name_en: "Hotel Double Bedding Set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 12,
|
||||||
|
brand: "Moleskine",
|
||||||
|
subcategory: "دفاتر",
|
||||||
|
name: "دفتر ملاحظات فاخر A5",
|
||||||
|
name_en: "Premium Notebook A5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 12,
|
||||||
|
brand: "Parker",
|
||||||
|
subcategory: "أقلام",
|
||||||
|
name: "قلم حبر أنيق هدية عملية",
|
||||||
|
name_en: "Elegant Ink Pen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 13,
|
||||||
|
brand: "Nikai",
|
||||||
|
subcategory: "غسالات",
|
||||||
|
name: "غسالة أوتوماتيك 8 كجم لون فضي",
|
||||||
|
name_en: "8kg Automatic Washer Silver",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 13,
|
||||||
|
brand: "Haier",
|
||||||
|
subcategory: "ثلاجات",
|
||||||
|
name: "ثلاجة بابين سعة 18 قدم",
|
||||||
|
name_en: "18ft Double Door Refrigerator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 14,
|
||||||
|
brand: "Apple",
|
||||||
|
subcategory: "هواتف آيفون",
|
||||||
|
name: "آيفون 17 برو ماكس تيتانيوم",
|
||||||
|
name_en: "iPhone 17 Pro Max Titanium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 14,
|
||||||
|
brand: "Anker",
|
||||||
|
subcategory: "شواحن وكفرات",
|
||||||
|
name: "شاحن سريع 65 واط مع كيبل",
|
||||||
|
name_en: "65W Fast Charger with Cable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 15,
|
||||||
|
brand: "ASUS",
|
||||||
|
subcategory: "لابتوبات",
|
||||||
|
name: "لابتوب Ryzen 16GB SSD 1TB",
|
||||||
|
name_en: "Ryzen Laptop 16GB 1TB SSD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 15,
|
||||||
|
brand: "Dell",
|
||||||
|
subcategory: "شاشات مكتبية",
|
||||||
|
name: "شاشة مكتبية 27 بوصة QHD",
|
||||||
|
name_en: "27-inch QHD Monitor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 16,
|
||||||
|
brand: "Fujifilm",
|
||||||
|
subcategory: "كاميرات فورية",
|
||||||
|
name: "كاميرا فورية وردية مع أفلام",
|
||||||
|
name_en: "Pink Instant Camera Bundle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 16,
|
||||||
|
brand: "Canon",
|
||||||
|
subcategory: "عدسات",
|
||||||
|
name: "عدسة تصوير 50mm فتحة واسعة",
|
||||||
|
name_en: "50mm Prime Lens",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 17,
|
||||||
|
brand: "Sony",
|
||||||
|
subcategory: "أجهزة ألعاب",
|
||||||
|
name: "جهاز PlayStation 5 إصدار رقمي",
|
||||||
|
name_en: "PlayStation 5 Digital Edition",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 17,
|
||||||
|
brand: "Razer",
|
||||||
|
subcategory: "إكسسوارات ألعاب",
|
||||||
|
name: "سماعة ألعاب محيطية RGB",
|
||||||
|
name_en: "RGB Surround Gaming Headset",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 18,
|
||||||
|
brand: "Coleman",
|
||||||
|
subcategory: "خيام",
|
||||||
|
name: "خيمة تخييم عائلية 4 أشخاص",
|
||||||
|
name_en: "4-Person Family Tent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 18,
|
||||||
|
brand: "Naturehike",
|
||||||
|
subcategory: "حقائب رحلات",
|
||||||
|
name: "حقيبة رحلات مقاومة للماء 45L",
|
||||||
|
name_en: "45L Waterproof Travel Backpack",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 19,
|
||||||
|
brand: "Afnan",
|
||||||
|
subcategory: "عطور شرقية",
|
||||||
|
name: "عطر رجالي فاخر بثبات طويل",
|
||||||
|
name_en: "Luxury Men's Perfume",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 19,
|
||||||
|
brand: "العود الملكي",
|
||||||
|
subcategory: "بخور",
|
||||||
|
name: "بخور معمول فاخر للمنزل",
|
||||||
|
name_en: "Luxury Incense Mamool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 20,
|
||||||
|
brand: "Gift Lab",
|
||||||
|
subcategory: "هدايا فاخرة",
|
||||||
|
name: "بوكس هدية فاخر للمناسبات",
|
||||||
|
name_en: "Luxury Gift Box",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 20,
|
||||||
|
brand: "Ribbon",
|
||||||
|
subcategory: "تغليف هدايا",
|
||||||
|
name: "طقم تغليف هدايا كامل",
|
||||||
|
name_en: "Complete Gift Wrapping Set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 21,
|
||||||
|
brand: "Pampers",
|
||||||
|
subcategory: "حفاضات",
|
||||||
|
name: "حفاضات أطفال مقاس 4 عبوة اقتصادية",
|
||||||
|
name_en: "Baby Diapers Size 4 Economy Pack",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 21,
|
||||||
|
brand: "Aptamil",
|
||||||
|
subcategory: "حليب أطفال",
|
||||||
|
name: "حليب أطفال المرحلة الثانية 800 جم",
|
||||||
|
name_en: "Infant Formula Stage 2 800g",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 22,
|
||||||
|
brand: "Vileda",
|
||||||
|
subcategory: "منظفات",
|
||||||
|
name: "منظف أرضيات برائحة منعشة",
|
||||||
|
name_en: "Fresh Floor Cleaner",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category_id: 22,
|
||||||
|
brand: "Sanita",
|
||||||
|
subcategory: "ورقيات",
|
||||||
|
name: "مناديل ورقية عائلية 10 عبوات",
|
||||||
|
name_en: "Family Tissue Pack 10 Rolls",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const CATEGORY_ART: Record<
|
||||||
|
number,
|
||||||
|
{ icon: string; bg: string; accent: string }
|
||||||
|
> = {
|
||||||
|
1: { icon: "📱", bg: "#eef6ff", accent: "#2563eb" },
|
||||||
|
2: { icon: "👗", bg: "#fff1f7", accent: "#db2777" },
|
||||||
|
3: { icon: "💄", bg: "#fff4f5", accent: "#e11d48" },
|
||||||
|
4: { icon: "🏠", bg: "#f5f3ff", accent: "#7c3aed" },
|
||||||
|
5: { icon: "🧸", bg: "#fff7ed", accent: "#ea580c" },
|
||||||
|
6: { icon: "🏋️", bg: "#ecfeff", accent: "#0891b2" },
|
||||||
|
7: { icon: "🚗", bg: "#f3f4f6", accent: "#374151" },
|
||||||
|
8: { icon: "📚", bg: "#fef3c7", accent: "#b45309" },
|
||||||
|
9: { icon: "🩺", bg: "#eff6ff", accent: "#1d4ed8" },
|
||||||
|
10: { icon: "👜", bg: "#fdf2f8", accent: "#be185d" },
|
||||||
|
11: { icon: "🛋️", bg: "#f5f5f4", accent: "#57534e" },
|
||||||
|
12: { icon: "✏️", bg: "#fefce8", accent: "#a16207" },
|
||||||
|
13: { icon: "🔌", bg: "#ecfccb", accent: "#4d7c0f" },
|
||||||
|
14: { icon: "📲", bg: "#ecfeff", accent: "#0f766e" },
|
||||||
|
15: { icon: "💻", bg: "#eef2ff", accent: "#4338ca" },
|
||||||
|
16: { icon: "📷", bg: "#faf5ff", accent: "#9333ea" },
|
||||||
|
17: { icon: "🎮", bg: "#111827", accent: "#22c55e" },
|
||||||
|
18: { icon: "⛺", bg: "#f0fdf4", accent: "#15803d" },
|
||||||
|
19: { icon: "🪔", bg: "#fff7ed", accent: "#c2410c" },
|
||||||
|
20: { icon: "🎁", bg: "#fdf2f8", accent: "#c026d3" },
|
||||||
|
21: { icon: "🍼", bg: "#eff6ff", accent: "#0284c7" },
|
||||||
|
22: { icon: "🧻", bg: "#fafaf9", accent: "#44403c" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function svgUri(svg: string) {
|
||||||
|
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeSvgText(value: string) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function productArt(
|
||||||
|
product: {
|
||||||
|
name: string;
|
||||||
|
brand: string | null;
|
||||||
|
subcategory: string | null;
|
||||||
|
category_id: number;
|
||||||
|
},
|
||||||
|
variant: number,
|
||||||
|
) {
|
||||||
|
const art = CATEGORY_ART[product.category_id] || {
|
||||||
|
icon: "🛍️",
|
||||||
|
bg: "#f4f4f5",
|
||||||
|
accent: "#f97316",
|
||||||
|
};
|
||||||
|
const badge =
|
||||||
|
variant === 1
|
||||||
|
? "واجهة المنتج"
|
||||||
|
: variant === 2
|
||||||
|
? "تفاصيل المنتج"
|
||||||
|
: "معاينة المتجر";
|
||||||
|
const safeName = escapeSvgText(product.name);
|
||||||
|
const safeBrand = escapeSvgText(String(product.brand || "EXTRA"));
|
||||||
|
const safeSub = escapeSvgText(String(product.subcategory || ""));
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="1100" viewBox="0 0 900 1100">
|
||||||
|
<rect width="900" height="1100" rx="48" fill="${art.bg}"/>
|
||||||
|
<rect x="36" y="36" width="828" height="1028" rx="40" fill="white" stroke="${art.accent}" stroke-opacity="0.18" stroke-width="3"/>
|
||||||
|
<circle cx="450" cy="320" r="170" fill="${art.accent}" fill-opacity="0.09"/>
|
||||||
|
<text x="450" y="360" text-anchor="middle" font-size="160">${art.icon}</text>
|
||||||
|
<rect x="76" y="92" width="220" height="48" rx="24" fill="${art.accent}"/>
|
||||||
|
<text x="186" y="124" text-anchor="middle" fill="white" font-size="24" font-family="Arial" font-weight="700">${safeBrand}</text>
|
||||||
|
<rect x="620" y="92" width="204" height="48" rx="24" fill="#111827" fill-opacity="0.08"/>
|
||||||
|
<text x="722" y="124" text-anchor="middle" fill="#111827" font-size="22" font-family="Arial" font-weight="700">${badge}</text>
|
||||||
|
<text x="450" y="620" text-anchor="middle" fill="#111827" font-size="42" font-family="Arial" font-weight="700">${safeName.slice(0, 32)}</text>
|
||||||
|
<text x="450" y="670" text-anchor="middle" fill="#6b7280" font-size="28" font-family="Arial">${safeSub.slice(0, 34)}</text>
|
||||||
|
<rect x="120" y="760" width="660" height="120" rx="28" fill="${art.accent}" fill-opacity="0.08"/>
|
||||||
|
<text x="450" y="815" text-anchor="middle" fill="${art.accent}" font-size="30" font-family="Arial" font-weight="700">صورة مطابقة لاسم المنتج داخل المعاينة</text>
|
||||||
|
<text x="450" y="858" text-anchor="middle" fill="#374151" font-size="24" font-family="Arial">${safeBrand} • ${safeSub.slice(0, 20)}</text>
|
||||||
|
<text x="450" y="995" text-anchor="middle" fill="#9ca3af" font-size="22" font-family="Arial">EXTRA Preview Catalog</text>
|
||||||
|
</svg>`;
|
||||||
|
return svgUri(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorPalette = ["أسود", "أبيض", "رمادي", "ذهبي", "وردي", "كحلي"];
|
||||||
|
const sizePalette = ["S", "M", "L", "XL", "مقاس حر"];
|
||||||
|
|
||||||
|
export const FALLBACK_PRODUCTS: FallbackProduct[] = PRODUCT_BLUEPRINTS.map(
|
||||||
|
(item, index) => {
|
||||||
|
const id = index + 1;
|
||||||
|
const color = colorPalette[index % colorPalette.length];
|
||||||
|
const size = sizePalette[index % sizePalette.length];
|
||||||
|
const price = 79 + index * 13;
|
||||||
|
const original = price + 20 + (index % 4) * 15;
|
||||||
|
const category = EXTRA_CATEGORIES.find(
|
||||||
|
(cat) => cat.id === item.category_id,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: item.name,
|
||||||
|
name_en: item.name_en,
|
||||||
|
brand: item.brand,
|
||||||
|
price: String(price),
|
||||||
|
original_price: String(original),
|
||||||
|
images: [1, 2, 3].map((imgIndex) => productArt(item, imgIndex)),
|
||||||
|
colors: [
|
||||||
|
color,
|
||||||
|
colorPalette[(index + 2) % colorPalette.length],
|
||||||
|
colorPalette[(index + 4) % colorPalette.length],
|
||||||
|
],
|
||||||
|
sizes:
|
||||||
|
item.category_id === 1 ||
|
||||||
|
item.category_id === 13 ||
|
||||||
|
item.category_id === 14 ||
|
||||||
|
item.category_id === 15 ||
|
||||||
|
item.category_id === 16 ||
|
||||||
|
item.category_id === 17
|
||||||
|
? ["قياسي", "إصدار 2026", "ضمان سنتين"]
|
||||||
|
: [
|
||||||
|
size,
|
||||||
|
sizePalette[(index + 1) % sizePalette.length],
|
||||||
|
sizePalette[(index + 2) % sizePalette.length],
|
||||||
|
],
|
||||||
|
specs: {
|
||||||
|
الفئة: category.name,
|
||||||
|
"الفئة الفرعية": item.subcategory,
|
||||||
|
"العلامة التجارية": item.brand,
|
||||||
|
اللون: color,
|
||||||
|
التوفر: "متوفر للشحن داخل السعودية",
|
||||||
|
الضمان: item.category_id <= 17 ? "ضمان سنتين" : "ضمان 7 أيام للاستبدال",
|
||||||
|
},
|
||||||
|
marketing_points: [
|
||||||
|
`مصمم ليلائم احتياج السوق السعودي ضمن قسم ${category.name}.`,
|
||||||
|
"جودة موثوقة وسعر مناسب مع تجربة استخدام سهلة.",
|
||||||
|
"شحن سريع داخل الرياض وخيارات توصيل لباقي مناطق المملكة.",
|
||||||
|
],
|
||||||
|
subcategory: item.subcategory,
|
||||||
|
category_id: item.category_id,
|
||||||
|
rating: (4.4 + (index % 6) * 0.1).toFixed(1),
|
||||||
|
review_count: 24 + index * 7,
|
||||||
|
stock: 3 + (index % 11) * 4,
|
||||||
|
is_trending: index % 2 === 0,
|
||||||
|
is_bestseller: index % 3 === 0,
|
||||||
|
is_new: index % 4 === 0,
|
||||||
|
is_top_rated: index % 5 === 0 || index % 6 === 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FALLBACK_STORE_SETTINGS: Record<string, string> = {
|
||||||
|
announcement_enabled: "true",
|
||||||
|
announcement_text:
|
||||||
|
"⚡ تم استكمال عرض فئات المتجر والقوائم الأساسية داخل المعاينة",
|
||||||
|
announcement_text_en:
|
||||||
|
"⚡ Store categories and key menus are now visible in the preview",
|
||||||
|
announcement_color: "#f97316",
|
||||||
|
announcement_text_color: "#ffffff",
|
||||||
|
hero_enabled: "true",
|
||||||
|
hero_badge_ar: "⚡ متجر سعودي شامل — 22 فئة رئيسية",
|
||||||
|
hero_badge_en: "⚡ Saudi Store — 22 Main Categories",
|
||||||
|
hero_title_ar: "كل فئات المتجر\nفي مكان واحد",
|
||||||
|
hero_title_en: "All store categories\nin one place",
|
||||||
|
hero_subtitle_ar:
|
||||||
|
"أضفنا الفئات الرئيسية والقوائم الأساسية كما في الملف، لتظهر بوضوح في الصفحة الرئيسية والتذييل والتنقل العلوي.",
|
||||||
|
hero_subtitle_en:
|
||||||
|
"Main categories and key menus from the file are now visible across the homepage, header, and footer.",
|
||||||
|
hero_cta_ar: "تصفح الفئات",
|
||||||
|
hero_cta_en: "Browse Categories",
|
||||||
|
hero_cta_link: "/category/0",
|
||||||
|
hero_accent_color: "#f97316",
|
||||||
|
extra_section_enabled: "true",
|
||||||
|
extra_section_title_ar: "فئات المنتجات الرئيسية",
|
||||||
|
extra_section_title_en: "Main Product Categories",
|
||||||
|
shein_section_enabled: "true",
|
||||||
|
shein_section_title_ar: "قوائم الأزياء والجمال",
|
||||||
|
shein_section_title_en: "Fashion & Beauty Menus",
|
||||||
|
section_trending_enabled: "true",
|
||||||
|
section_trending_title_ar: "الأكثر رواجاً في السعودية",
|
||||||
|
section_trending_title_en: "Trending in Saudi Arabia",
|
||||||
|
section_bestseller_enabled: "true",
|
||||||
|
section_bestseller_title_ar: "الأكثر مبيعاً",
|
||||||
|
section_bestseller_title_en: "Best Sellers",
|
||||||
|
section_new_enabled: "true",
|
||||||
|
section_new_title_ar: "وصل حديثاً",
|
||||||
|
section_new_title_en: "New Arrivals",
|
||||||
|
section_top_rated_enabled: "true",
|
||||||
|
section_top_rated_title_ar: "أعلى تقييماً",
|
||||||
|
section_top_rated_title_en: "Top Rated",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FEATURED_MENU_ITEMS = [
|
||||||
|
{
|
||||||
|
id: "trending",
|
||||||
|
label_ar: "الأكثر رواجاً",
|
||||||
|
label_en: "Trending",
|
||||||
|
href: "/#section-trending",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "popular",
|
||||||
|
label_ar: "الأكثر طلباً",
|
||||||
|
label_en: "Most Wanted",
|
||||||
|
href: "/#section-bestseller",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bestseller",
|
||||||
|
label_ar: "الأكثر مبيعاً",
|
||||||
|
label_en: "Best Sellers",
|
||||||
|
href: "/#section-bestseller",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "famous",
|
||||||
|
label_ar: "الأكثر شهرة",
|
||||||
|
label_en: "Most Popular",
|
||||||
|
href: "/#section-top-rated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ksa-trend",
|
||||||
|
label_ar: "الترند في السوق السعودي",
|
||||||
|
label_en: "Saudi Market Trend",
|
||||||
|
href: "/#section-trending",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FOOTER_POLICY_LINKS = [
|
||||||
|
{ href: "/category/0", label_ar: "سياسة الشحن", label_en: "Shipping Policy" },
|
||||||
|
{ href: "/checkout", label_ar: "الدفع الآمن", label_en: "Secure Checkout" },
|
||||||
|
{ href: "/profile", label_ar: "حسابي", label_en: "My Account" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getFallbackCategories() {
|
||||||
|
return [...FALLBACK_CATEGORIES].sort(
|
||||||
|
(a, b) => (a.sort_order || 0) - (b.sort_order || 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFallbackCategoryTree() {
|
||||||
|
return FALLBACK_CATEGORY_TREE.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: [...node.children],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFallbackProduct(id: number) {
|
||||||
|
return (
|
||||||
|
FALLBACK_PRODUCTS.find((product) => product.id === id) ||
|
||||||
|
FALLBACK_PRODUCTS[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFallbackProducts(
|
||||||
|
params: Record<string, string | number | undefined>,
|
||||||
|
): FallbackProductsResponse {
|
||||||
|
let products = [...FALLBACK_PRODUCTS];
|
||||||
|
const categoryId = Number(params.category_id || 0);
|
||||||
|
const search = String(params.search || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
const featured = String(params.featured || "");
|
||||||
|
const subcategory = String(params.subcategory || "").trim();
|
||||||
|
const page = Number(params.page || 1);
|
||||||
|
const limit = Number(params.limit || 20);
|
||||||
|
|
||||||
|
if (categoryId > 0)
|
||||||
|
products = products.filter((p) => p.category_id === categoryId);
|
||||||
|
if (search) {
|
||||||
|
products = products.filter((p) =>
|
||||||
|
[p.name, p.name_en, p.brand, p.subcategory]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(search)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (subcategory)
|
||||||
|
products = products.filter((p) => p.subcategory === subcategory);
|
||||||
|
if (featured === "trending") products = products.filter((p) => p.is_trending);
|
||||||
|
if (featured === "bestseller")
|
||||||
|
products = products.filter((p) => p.is_bestseller);
|
||||||
|
if (featured === "new_arrivals") products = products.filter((p) => p.is_new);
|
||||||
|
if (featured === "top_rated")
|
||||||
|
products = products.filter((p) => p.is_top_rated);
|
||||||
|
|
||||||
|
const total = products.length;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.slice(start, start + limit),
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
total_pages: totalPages,
|
||||||
|
};
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -4,13 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
||||||
|
|
||||||
const rawPort = process.env.PORT;
|
const rawPort = process.env.PORT ?? "3001";
|
||||||
|
|
||||||
if (!rawPort) {
|
|
||||||
throw new Error(
|
|
||||||
"PORT environment variable is required but was not provided.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = Number(rawPort);
|
const port = Number(rawPort);
|
||||||
|
|
||||||
@ -18,13 +12,7 @@ if (Number.isNaN(port) || port <= 0) {
|
|||||||
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = process.env.BASE_PATH;
|
const basePath = process.env.BASE_PATH ?? "/";
|
||||||
|
|
||||||
if (!basePath) {
|
|
||||||
throw new Error(
|
|
||||||
"BASE_PATH environment variable is required but was not provided.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: basePath,
|
base: basePath,
|
||||||
|
|||||||
@ -5,13 +5,7 @@ import path from "path";
|
|||||||
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
||||||
import { mockupPreviewPlugin } from "./mockupPreviewPlugin";
|
import { mockupPreviewPlugin } from "./mockupPreviewPlugin";
|
||||||
|
|
||||||
const rawPort = process.env.PORT;
|
const rawPort = process.env.PORT ?? "3001";
|
||||||
|
|
||||||
if (!rawPort) {
|
|
||||||
throw new Error(
|
|
||||||
"PORT environment variable is required but was not provided.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = Number(rawPort);
|
const port = Number(rawPort);
|
||||||
|
|
||||||
@ -19,13 +13,7 @@ if (Number.isNaN(port) || port <= 0) {
|
|||||||
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = process.env.BASE_PATH;
|
const basePath = process.env.BASE_PATH ?? "/";
|
||||||
|
|
||||||
if (!basePath) {
|
|
||||||
throw new Error(
|
|
||||||
"BASE_PATH environment variable is required but was not provided.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: basePath,
|
base: basePath,
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from "./generated/api";
|
export * from "./generated/api";
|
||||||
export * from "./generated/types";
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user