/* ============================================================ TECHZONE STORE - Cloudflare Worker (Single File) Database: GitHub (ex1zx/Golden) Real-time: SSE + Durable Object ============================================================ */ // ───────────────────────────────────────────── // DURABLE OBJECT: StoreNotifier // ───────────────────────────────────────────── export class StoreNotifier { constructor(state, env) { this.state = state; this.sessions = new Map(); } async fetch(request) { const url = new URL(request.url); if (url.pathname === '/sse') return this._handleSSE(request); if (url.pathname === '/broadcast' && request.method === 'POST') return this._handleBroadcast(request); return new Response('not found', { status: 404 }); } _handleSSE(request) { const sessionId = crypto.randomUUID(); const encoder = new TextEncoder(); const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); this.sessions.set(sessionId, writer); writer.write(encoder.encode(`data: ${JSON.stringify({ type: 'ping' })}\n\n`)); const heartbeat = setInterval(async () => { try { await writer.write(encoder.encode(`: heartbeat\n\n`)); } catch { clearInterval(heartbeat); this.sessions.delete(sessionId); } }, 25000); request.signal?.addEventListener('abort', () => { clearInterval(heartbeat); this.sessions.delete(sessionId); try { writer.close(); } catch {} }); return new Response(readable, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', }, }); } async _handleBroadcast(request) { const data = await request.json(); const encoder = new TextEncoder(); const msg = encoder.encode(`data: ${JSON.stringify(data)}\n\n`); const dead = []; for (const [id, writer] of this.sessions) { try { await writer.write(msg); } catch { dead.push(id); } } dead.forEach(id => this.sessions.delete(id)); return new Response('ok'); } } // ───────────────────────────────────────────── // GITHUB DATABASE // ───────────────────────────────────────────── const GH_OWNER = 'ex1zx'; const GH_REPO = 'Golden'; const GH_API = `https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents`; function ghHeaders(token) { return { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TechZone-CF-Worker', 'Content-Type': 'application/json', }; } // Read a JSON file from GitHub. Returns { data, sha } or null if not found. async function ghRead(env, path) { const res = await fetch(`${GH_API}/${path}`, { headers: ghHeaders(env.GITHUB_TOKEN) }); if (!res.ok) return null; const file = await res.json(); try { const decoded = new TextDecoder().decode( Uint8Array.from(atob(file.content.replace(/\n/g, '')), c => c.charCodeAt(0)) ); return { data: JSON.parse(decoded), sha: file.sha }; } catch { return null; } } // Write a JSON file to GitHub. Automatically creates it (and any missing parent // "directories") if it does not exist yet. Retries once on SHA conflict (409). async function ghWrite(env, path, data, sha) { const jsonStr = JSON.stringify(data, null, 2); const encoded = btoa(unescape(encodeURIComponent(jsonStr))); const doWrite = async (currentSha) => { const body = { message: `update ${path}`, content: encoded }; if (currentSha) body.sha = currentSha; return fetch(`${GH_API}/${path}`, { method: 'PUT', headers: ghHeaders(env.GITHUB_TOKEN), body: JSON.stringify(body), }); }; let res = await doWrite(sha); // 409 Conflict → SHA is stale; fetch fresh SHA and retry once if (res.status === 409) { const fresh = await ghRead(env, path); res = await doWrite(fresh?.sha); } return res.ok; } // Read a file and guarantee it exists with `defaultValue` if missing. // Returns { data, sha } always. async function ghEnsure(env, path, defaultValue) { const existing = await ghRead(env, path); if (existing) return existing; // File doesn't exist → create it now (GitHub creates parent dirs automatically) await ghWrite(env, path, defaultValue, null); return { data: defaultValue, sha: null }; } // Upload a raw image (base64) to uploads/ and return its raw.githubusercontent URL. async function ghUploadImage(env, filename, base64Data) { const path = `uploads/${filename}`; // Fetch current SHA if file already exists (to avoid conflict) const existing = await fetch(`${GH_API}/${path}`, { headers: ghHeaders(env.GITHUB_TOKEN) }); let sha; if (existing.ok) { const f = await existing.json(); sha = f.sha; } const body = { message: `upload image ${filename}`, content: base64Data }; if (sha) body.sha = sha; const res = await fetch(`${GH_API}/${path}`, { method: 'PUT', headers: ghHeaders(env.GITHUB_TOKEN), body: JSON.stringify(body), }); if (!res.ok) return null; return `https://raw.githubusercontent.com/${GH_OWNER}/${GH_REPO}/main/${path}`; } // ───────────────────────────────────────────── // INITIALISE ALL DATA FILES (once, on demand) // ───────────────────────────────────────────── // Called only when the Worker needs to read/write data. // Creates missing files so the GitHub repo doesn't need manual setup. async function initDataFiles(env) { // Run all ensures in parallel — each one creates the file only if absent await Promise.all([ ghEnsure(env, 'data/products.json', []), ghEnsure(env, 'data/categories.json', []), ghEnsure(env, 'data/ads.json', []), ghEnsure(env, 'data/orders.json', []), // Admin user bootstrapped from ADMIN_PASSWORD env var (or default) (async () => { const r = await ghRead(env, 'data/users.json'); if (!r) { const adminPwd = env.ADMIN_PASSWORD || 'admin123'; const hashed = await hashPwd(adminPwd); await ghWrite(env, 'data/users.json', [ { id: 1, phone: '911', name: 'المدير', password: hashed, suspicious: false, is_admin: true }, ], null); } })(), ]); } // ───────────────────────────────────────────── // BOOT CACHE (module-level, persists across requests in same isolate) // ───────────────────────────────────────────── let _booted = false; // true after first successful initDataFiles() // ───────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────── async function hashPwd(pwd) { const data = new TextEncoder().encode(pwd + 'tz_salt_2025'); const buf = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); } async function verifyRecaptcha(token, secret) { try { const params = new URLSearchParams({ secret, response: token }); const res = await fetch('https://www.google.com/recaptcha/api/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), }); const d = await res.json(); return d.success === true; } catch { return false; } } async function broadcast(env, data) { try { const id = env.STORE_NOTIFIER.idFromName('global'); const stub = env.STORE_NOTIFIER.get(id); await stub.fetch(new Request('http://internal/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), })); } catch {} } function json(data, status = 200, extra = {}) { return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json', ...extra }, }); } function getDate() { const n = new Date(); return `${String(n.getDate()).padStart(2,'0')}/${String(n.getMonth()+1).padStart(2,'0')}/${n.getFullYear()}`; } // ───────────────────────────────────────────── // MAIN WORKER // ───────────────────────────────────────────── export default { async fetch(request, env) { const url = new URL(request.url); const path = url.pathname; const method = request.method; if (method === 'OPTIONS') { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }}); } // ── ONE-TIME BOOT: ensure all GitHub data files exist ──────── // Only runs once per isolate lifetime (cached in module scope). if (!_booted && path !== '/api/sse') { try { await initDataFiles(env); _booted = true; } catch { /* network hiccup – will retry next request */ } } // ── SSE ────────────────────────────────── if (path === '/api/sse') { const id = env.STORE_NOTIFIER.idFromName('global'); const stub = env.STORE_NOTIFIER.get(id); return stub.fetch(new Request('http://internal/sse', request)); } // ── PRODUCTS ───────────────────────────── if (path === '/api/products' && method === 'GET') { const cat = url.searchParams.get('category'); const r = await ghRead(env, 'data/products.json'); let products = r?.data || []; if (cat) products = products.filter(p => p.category === cat); return json({ products }); } // ── CATEGORIES ─────────────────────────── if (path === '/api/categories' && method === 'GET') { const r = await ghRead(env, 'data/categories.json'); return json({ categories: r?.data || [] }); } // ── ADS ────────────────────────────────── if (path === '/api/ads' && method === 'GET') { const r = await ghRead(env, 'data/ads.json'); return json({ ads: r?.data || [] }); } // ── BULK DATA (products + categories + ads) ── if (path === '/api/data' && method === 'GET') { const [pr, cr, ar] = await Promise.all([ ghRead(env, 'data/products.json'), ghRead(env, 'data/categories.json'), ghRead(env, 'data/ads.json'), ]); return json({ products: pr?.data||[], categories: cr?.data||[], ads: ar?.data||[] }); } // ── UPLOAD ─────────────────────────────── if (path === '/api/upload' && method === 'POST') { try { const formData = await request.formData(); const file = formData.get('image'); if (!file) return json({ error: 'no_file' }, 400); const ab = await file.arrayBuffer(); const b64 = btoa(String.fromCharCode(...new Uint8Array(ab))); const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg'; const fn = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`; const imgUrl = await ghUploadImage(env, fn, b64); if (!imgUrl) return json({ error: 'upload_failed' }, 500); return json({ url: imgUrl }); } catch { return json({ error: 'upload_failed' }, 500); } } // ── AUTH: REGISTER ─────────────────────── if (path === '/api/auth/register' && method === 'POST') { const { phone, name, password, recaptchaToken } = await request.json(); if (!recaptchaToken) return json({ error: 'recaptcha_failed' }, 400); const captchaSecret = env.RECAPTCHA_SECRET || '6LeIxAcTAAAAAGG-vFI1TnRWxMBNFkWwI1ahLdTw'; const ok = await verifyRecaptcha(recaptchaToken, captchaSecret); if (!ok) return json({ error: 'recaptcha_failed' }, 400); if (!phone || !name || !password) return json({ error: 'missing_fields' }, 400); const phoneClean = phone.replace(/\s/g, ''); if (phoneClean.replace(/\D/g, '').length < 9) return json({ error: 'invalid_phone' }, 400); const r = await ghRead(env, 'data/users.json'); const users = r?.data || []; if (users.find(u => u.phone === phoneClean)) return json({ error: 'phone_exists' }, 409); const hashed = await hashPwd(password); users.push({ id: Date.now(), phone: phoneClean, name: name.trim(), password: hashed, suspicious: false, is_admin: false }); await ghWrite(env, 'data/users.json', users, r?.sha); return json({ success: true, user: { phone: phoneClean, name: name.trim(), suspicious: false, is_admin: false } }); } // ── AUTH: LOGIN ────────────────────────── if (path === '/api/auth/login' && method === 'POST') { const { phone, password, recaptchaToken } = await request.json(); if (!phone || !password) return json({ error: 'missing_fields' }, 400); const phoneClean = phone.replace(/\s/g, ''); const r = await ghRead(env, 'data/users.json'); const users = r?.data || []; if (phoneClean === '911') { const admin = users.find(u => u.phone === '911'); const hashed = await hashPwd(password); // If no admin record yet → bootstrap from ADMIN_PASSWORD env var (or default) if (!admin) { const adminPwd = env.ADMIN_PASSWORD || 'admin123'; if (password !== adminPwd) return json({ error: 'wrong_password' }, 401); // Create the users file with admin user now const newHashed = await hashPwd(adminPwd); const r2 = await ghRead(env, 'data/users.json'); const newUsers = [...(r2?.data || [])]; newUsers.push({ id: 1, phone: '911', name: 'المدير', password: newHashed, suspicious: false, is_admin: true }); await ghWrite(env, 'data/users.json', newUsers, r2?.sha); return json({ success: true, user: { phone: '911', name: 'المدير', suspicious: false, is_admin: true } }); } if (admin.password !== hashed) return json({ error: 'wrong_password' }, 401); return json({ success: true, user: { phone: '911', name: admin.name || 'المدير', suspicious: false, is_admin: true } }); } if (!recaptchaToken) return json({ error: 'recaptcha_failed' }, 400); const captchaSecret = env.RECAPTCHA_SECRET || '6LeIxAcTAAAAAGG-vFI1TnRWxMBNFkWwI1ahLdTw'; const ok = await verifyRecaptcha(recaptchaToken, captchaSecret); if (!ok) return json({ error: 'recaptcha_failed' }, 400); const user = users.find(u => u.phone === phoneClean); if (!user) return json({ error: 'not_found' }, 401); const hashed = await hashPwd(password); if (user.password !== hashed) return json({ error: 'wrong_password' }, 401); return json({ success: true, user: { phone: user.phone, name: user.name, suspicious: user.suspicious, is_admin: user.is_admin || false } }); } // ── AUTH: FLAG SUSPICIOUS ──────────────── if (path === '/api/auth/flag-suspicious' && method === 'POST') { const { phone } = await request.json(); if (!phone) return json({ error: 'phone required' }, 400); const r = await ghRead(env, 'data/users.json'); const users = (r?.data || []).map(u => u.phone === phone ? { ...u, suspicious: true, suspicious_verified: false } : u); await ghWrite(env, 'data/users.json', users, r?.sha); return json({ success: true }); } // ── AUTH: CLEAR SUSPICIOUS ─────────────── if (path === '/api/auth/clear-suspicious' && method === 'POST') { const { phone, recaptchaToken } = await request.json(); if (!phone || !recaptchaToken) return json({ error: 'missing_fields' }, 400); const captchaSecret = env.RECAPTCHA_SECRET || '6LeIxAcTAAAAAGG-vFI1TnRWxMBNFkWwI1ahLdTw'; const ok = await verifyRecaptcha(recaptchaToken, captchaSecret); if (!ok) return json({ error: 'recaptcha_failed' }, 400); const r = await ghRead(env, 'data/users.json'); const users = (r?.data || []).map(u => u.phone === phone ? { ...u, suspicious: false, suspicious_verified: true } : u); await ghWrite(env, 'data/users.json', users, r?.sha); return json({ success: true }); } // ── AUTH: PROFILE UPDATE ───────────────── if (path === '/api/auth/profile' && method === 'PATCH') { const { phone, newName, newPhone } = await request.json(); if (!phone) return json({ error: 'phone required' }, 400); const r = await ghRead(env, 'data/users.json'); let users = r?.data || []; if (newPhone) { const clean = newPhone.replace(/\s/g, ''); if (clean.replace(/\D/g, '').length < 9) return json({ error: 'invalid_phone' }, 400); if (users.find(u => u.phone === clean && u.phone !== phone)) return json({ error: 'phone_exists' }, 409); users = users.map(u => u.phone === phone ? { ...u, phone: clean } : u); // update orders const or = await ghRead(env, 'data/orders.json'); if (or) { const orders = (or.data || []).map(o => o.user_phone === phone ? { ...o, user_phone: clean } : o); await ghWrite(env, 'data/orders.json', orders, or.sha); } } if (newName) { const targetPhone = newPhone ? newPhone.replace(/\s/g, '') : phone; users = users.map(u => u.phone === targetPhone ? { ...u, name: newName.trim() } : u); } await ghWrite(env, 'data/users.json', users, r?.sha); const targetPhone = newPhone ? newPhone.replace(/\s/g, '') : phone; const updated = users.find(u => u.phone === targetPhone); if (!updated) return json({ error: 'not_found' }, 404); return json({ success: true, user: { phone: updated.phone, name: updated.name } }); } // ── AUTH: STATUS ───────────────────────── if (path === '/api/auth/status' && method === 'GET') { const phone = url.searchParams.get('phone'); if (!phone) return json({ error: 'phone required' }, 400); const r = await ghRead(env, 'data/users.json'); const user = (r?.data || []).find(u => u.phone === phone); if (!user) return json({ error: 'not_found' }, 404); return json({ suspicious: user.suspicious, suspicious_verified: user.suspicious_verified }); } // ── ORDERS: CREATE ─────────────────────── if (path === '/api/orders' && method === 'POST') { const { userPhone, deliveryPhone, address, notes, total, items } = await request.json(); if (!userPhone || !deliveryPhone || !address || !total || !items?.length) return json({ error: 'missing_fields' }, 400); const r = await ghRead(env, 'data/orders.json'); const orders = r?.data || []; const newId = Date.now(); orders.push({ id: newId, user_phone: userPhone, delivery_phone: deliveryPhone, address, notes: notes || '', total, status: 'قيد المعالجة', date: getDate(), items, rejection_reason: null, rejected_at: null, rejection_notified: false, created_at: new Date().toISOString(), }); await ghWrite(env, 'data/orders.json', orders, r?.sha); await broadcast(env, { type: 'orders_updated' }); return json({ success: true, orderId: newId }); } // ── ORDERS: GET USER ORDERS ────────────── if (path === '/api/orders' && method === 'GET') { const phone = url.searchParams.get('phone'); if (!phone) return json({ error: 'phone required' }, 400); const r = await ghRead(env, 'data/orders.json'); const orders = (r?.data || []).filter(o => o.user_phone === phone).reverse(); return json({ orders }); } // ── ORDERS: GET SINGLE ─────────────────── const orderDetailMatch = path.match(/^\/api\/orders\/(\d+)$/); if (orderDetailMatch && method === 'GET') { const id = parseInt(orderDetailMatch[1]); const phone = url.searchParams.get('phone'); const r = await ghRead(env, 'data/orders.json'); const order = (r?.data || []).find(o => o.id === id && o.user_phone === phone); if (!order) return json({ error: 'not_found' }, 404); return json({ order, items: order.items || [] }); } // ── ORDERS: STATUS ─────────────────────── const orderStatusMatch = path.match(/^\/api\/orders\/(\d+)\/status$/); if (orderStatusMatch && method === 'PATCH') { const id = parseInt(orderStatusMatch[1]); const { status, adminPhone } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const allowed = ['قيد المعالجة', 'تم التوصيل']; if (!status || !allowed.includes(status)) return json({ error: 'invalid_status' }, 400); const r = await ghRead(env, 'data/orders.json'); const orders = (r?.data || []).map(o => o.id === id ? { ...o, status } : o); await ghWrite(env, 'data/orders.json', orders, r?.sha); await broadcast(env, { type: 'orders_updated' }); return json({ success: true }); } // ── ORDERS: REJECT ─────────────────────── const orderRejectMatch = path.match(/^\/api\/orders\/(\d+)\/reject$/); if (orderRejectMatch && method === 'PATCH') { const id = parseInt(orderRejectMatch[1]); const { reason, adminPhone } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); if (!reason?.trim()) return json({ error: 'reason required' }, 400); const r = await ghRead(env, 'data/orders.json'); const orders = (r?.data || []).map(o => o.id === id ? { ...o, status: 'مرفوض', rejection_reason: reason.trim(), rejected_at: new Date().toISOString(), rejection_notified: false } : o ); await ghWrite(env, 'data/orders.json', orders, r?.sha); await broadcast(env, { type: 'orders_updated' }); return json({ success: true }); } // ── ADMIN: ALL ORDERS ──────────────────── if (path === '/api/admin/orders' && method === 'GET') { const adminPhone = url.searchParams.get('adminPhone'); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const [or, ur] = await Promise.all([ghRead(env, 'data/orders.json'), ghRead(env, 'data/users.json')]); const orders = (or?.data || []).reverse(); const users = ur?.data || []; const enriched = orders.map(o => { const u = users.find(u => u.phone === o.user_phone); return { ...o, user_name: u?.name || o.user_phone }; }); return json({ orders: enriched }); } // ── NOTIFICATIONS ──────────────────────── const notifMatch = path.match(/^\/api\/notifications\/(.+)$/); if (notifMatch && method === 'GET') { const phone = decodeURIComponent(notifMatch[1]); const r = await ghRead(env, 'data/orders.json'); const rejections = (r?.data || []) .filter(o => o.user_phone === phone && o.status === 'مرفوض' && !o.rejection_notified) .map(o => ({ id: o.id, total: o.total, date: o.date, rejection_reason: o.rejection_reason, rejected_at: o.rejected_at, status: o.status })); return json({ rejections }); } const notifAckMatch = path.match(/^\/api\/notifications\/(.+)\/ack$/); if (notifAckMatch && method === 'POST') { const phone = decodeURIComponent(notifAckMatch[1]); const r = await ghRead(env, 'data/orders.json'); const orders = (r?.data || []).map(o => o.user_phone === phone && o.status === 'مرفوض' && !o.rejection_notified ? { ...o, rejection_notified: true } : o ); await ghWrite(env, 'data/orders.json', orders, r?.sha); return json({ success: true }); } // ── ADMIN: CATEGORIES ──────────────────── if (path === '/api/admin/categories' && method === 'POST') { const { adminPhone, slug, name, image } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); if (!slug || !name || !image) return json({ error: 'missing_fields' }, 400); const slugClean = slug.trim().toLowerCase().replace(/\s+/g, '-'); const r = await ghRead(env, 'data/categories.json'); const cats = r?.data || []; if (cats.find(c => c.id === slugClean)) return json({ error: 'slug_exists' }, 409); const newCat = { id: slugClean, name: name.trim(), image: image.trim() }; cats.push(newCat); await ghWrite(env, 'data/categories.json', cats, r?.sha); // ↓ Send full updated array in SSE — zero extra fetch needed on clients await broadcast(env, { type: 'categories_updated', data: cats }); return json({ success: true, category: newCat }); } const adminCatMatch = path.match(/^\/api\/admin\/categories\/(.+)$/); if (adminCatMatch && method === 'PUT') { const slug = adminCatMatch[1]; const { adminPhone, name, image } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/categories.json'); let cats = r?.data || []; const idx = cats.findIndex(c => c.id === slug); if (idx === -1) return json({ error: 'not_found' }, 404); cats[idx] = { ...cats[idx], name: name.trim(), image: image.trim() }; await ghWrite(env, 'data/categories.json', cats, r?.sha); await broadcast(env, { type: 'categories_updated', data: cats }); return json({ success: true, category: cats[idx] }); } if (adminCatMatch && method === 'DELETE') { const slug = adminCatMatch[1]; const { adminPhone } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/categories.json'); const cats = (r?.data || []).filter(c => c.id !== slug); await ghWrite(env, 'data/categories.json', cats, r?.sha); await broadcast(env, { type: 'categories_updated', data: cats }); return json({ success: true }); } // ── ADMIN: PRODUCTS ────────────────────── if (path === '/api/admin/products' && method === 'POST') { const { adminPhone, name, price, oldPrice, image, category, rating, reviews, badge, description } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); if (!name || !price || !image || !category || !description) return json({ error: 'missing_fields' }, 400); const r = await ghRead(env, 'data/products.json'); const products = r?.data || []; const newId = products.length ? Math.max(...products.map(p => p.id)) + 1 : 1; const newProd = { id: newId, name: name.trim(), price, oldPrice: oldPrice || null, image: image.trim(), category: category.trim(), rating: rating || 4.5, reviews: reviews || 0, badge: badge?.trim() || null, description: description.trim() }; products.push(newProd); await ghWrite(env, 'data/products.json', products, r?.sha); // ↓ Full array in SSE event — clients update instantly, zero extra requests await broadcast(env, { type: 'products_updated', data: products }); return json({ success: true, product: newProd }); } const adminProdMatch = path.match(/^\/api\/admin\/products\/(\d+)$/); if (adminProdMatch && method === 'PUT') { const id = parseInt(adminProdMatch[1]); const { adminPhone, name, price, oldPrice, image, category, rating, reviews, badge, description } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/products.json'); let products = r?.data || []; const idx = products.findIndex(p => p.id === id); if (idx === -1) return json({ error: 'not_found' }, 404); products[idx] = { ...products[idx], name: name.trim(), price, oldPrice: oldPrice || null, image: image.trim(), category: category.trim(), rating: rating || 4.5, reviews: reviews || 0, badge: badge?.trim() || null, description: description.trim() }; await ghWrite(env, 'data/products.json', products, r?.sha); await broadcast(env, { type: 'products_updated', data: products }); return json({ success: true, product: products[idx] }); } if (adminProdMatch && method === 'DELETE') { const id = parseInt(adminProdMatch[1]); const { adminPhone } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/products.json'); const products = (r?.data || []).filter(p => p.id !== id); await ghWrite(env, 'data/products.json', products, r?.sha); await broadcast(env, { type: 'products_updated', data: products }); return json({ success: true }); } // ── ADMIN: ADS ─────────────────────────── if (path === '/api/admin/ads' && method === 'POST') { const { adminPhone, title, subtitle, image, gradient } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); if (!image) return json({ error: 'missing_fields' }, 400); const r = await ghRead(env, 'data/ads.json'); const ads = r?.data || []; const newId = ads.length ? Math.max(...ads.map(a => a.id)) + 1 : 1; const newAd = { id: newId, title: title || '', subtitle: subtitle || '', image: image.trim(), gradient: gradient || 'from-blue-600/85 to-background' }; ads.push(newAd); await ghWrite(env, 'data/ads.json', ads, r?.sha); await broadcast(env, { type: 'ads_updated', data: ads }); return json({ success: true, ad: newAd }); } const adminAdMatch = path.match(/^\/api\/admin\/ads\/(\d+)$/); if (adminAdMatch && method === 'PUT') { const id = parseInt(adminAdMatch[1]); const { adminPhone, title, subtitle, image, gradient } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/ads.json'); let ads = r?.data || []; const idx = ads.findIndex(a => a.id === id); if (idx === -1) return json({ error: 'not_found' }, 404); ads[idx] = { ...ads[idx], title: title || '', subtitle: subtitle || '', image: image.trim(), gradient: gradient || 'from-blue-600/85 to-background' }; await ghWrite(env, 'data/ads.json', ads, r?.sha); await broadcast(env, { type: 'ads_updated', data: ads }); return json({ success: true, ad: ads[idx] }); } if (adminAdMatch && method === 'DELETE') { const id = parseInt(adminAdMatch[1]); const { adminPhone } = await request.json(); if (adminPhone !== '911') return json({ error: 'forbidden' }, 403); const r = await ghRead(env, 'data/ads.json'); const ads = (r?.data || []).filter(a => a.id !== id); await ghWrite(env, 'data/ads.json', ads, r?.sha); await broadcast(env, { type: 'ads_updated', data: ads }); return json({ success: true }); } // ── SERVE HTML ──────────────────────────── if (method === 'GET') { // Load everything at once and embed in HTML const [pr, cr, ar] = await Promise.all([ ghRead(env, 'data/products.json'), ghRead(env, 'data/categories.json'), ghRead(env, 'data/ads.json'), ]); const initialData = { products: pr?.data || [], categories: cr?.data || [], ads: ar?.data || [], }; return new Response(buildHTML(initialData), { headers: { 'Content-Type': 'text/html;charset=utf-8', 'Cache-Control': 'no-store' }, }); } return new Response('not found', { status: 404 }); }, }; // ───────────────────────────────────────────── // HTML BUILDER — Complete Vanilla JS SPA // ───────────────────────────────────────────── function buildHTML(initialData) { const dataScript = ` ${dataScript}

تيكزون

جاري التحميل...0%
سلة المشتريات
`; }