Autosave: 20260328-015333

This commit is contained in:
Flatlogic Bot 2026-03-28 01:53:33 +00:00
parent 3d23e78133
commit de5aa451c1
12 changed files with 10484 additions and 1751 deletions

View File

@ -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();

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View 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("بيانات الدخول غير صحيحة");
}

View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
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

View File

@ -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,

View File

@ -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,

View File

@ -1,2 +1 @@
export * from "./generated/api"; export * from "./generated/api";
export * from "./generated/types";