Initial commit from saas-meta

This commit is contained in:
Hamza-Ayed
2026-03-30 17:04:27 +03:00
commit 3b28389dc3
91 changed files with 20697 additions and 0 deletions

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

348
frontend/index.html Normal file
View File

@@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SaaS Meta Ads - لوحة التحكم الاحترافية</title>
<link rel="stylesheet" href="src/style.css">
<!-- Google Fonts: Inter & Arabic font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=Noto+Kufi+Arabic:wght@300;400;600;700&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Flatpickr for better date picking -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" type="text/css" href="https://npmcdn.com/flatpickr/dist/themes/dark.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ar.js"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ar.js"></script>
<script type="module" src="src/auth.js"></script>
</head>
<body class="dark-theme">
<!-- Login Overlay | واجهة تسجيل الدخول -->
<div id="login-overlay" class="login-overlay">
<div class="login-card glass-card">
<div class="logo-section" style="margin-bottom: 2rem; justify-content: center;">
<div class="logo-icon">
<i data-lucide="shield-check"></i>
</div>
<span>تسجيل الدخول للنظام</span>
</div>
<p style="text-align: center; color: var(--text-secondary); margin-bottom: 2rem;">أدخل بريدك الإلكتروني للبدء في تحليل حملاتك</p>
<form id="login-form">
<div class="form-group">
<label>البريد الإلكتروني</label>
<input type="email" id="login-email" placeholder="example@mail.com" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; padding: 14px;">
دخول للمنصة
</button>
<div style="text-align: center; margin: 1rem 0; color: var(--text-secondary); font-size: 0.8rem;">أو</div>
<button type="button" id="btn-google-login" class="btn btn-secondary" style="width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px; background: white; color: #444; border: 1px solid #ddd; padding: 12px; font-weight: 600;">
<img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" width="18" height="18">
التسجيل عبر جوجل
</button>
</form>
<div id="login-error" class="error-text hidden" style="margin-top: 1rem; color: var(--danger); text-align: center;"></div>
</div>
</div>
<div class="app-container hidden" id="main-app">
<!-- Sidebar -->
<aside class="sidebar">
<div class="logo-section">
<div class="logo-icon">
<i data-lucide="layout-dashboard"></i>
</div>
<span>SaaS Meta Pro</span>
</div>
<nav class="side-nav">
<a href="#" class="nav-item active" data-page="dashboard">
<i data-lucide="line-chart"></i>
<span>الإحصائيات</span>
</a>
<a href="#" class="nav-item" data-page="connect">
<i data-lucide="link"></i>
<span>ربط الحسابات</span>
</a>
<a href="#" class="nav-item" data-page="ai-lab">
<i data-lucide="sparkles"></i>
<span>مختبر الذكاء الاصطناعي</span>
</a>
<a href="#" class="nav-item" data-page="automation">
<i data-lucide="settings-2"></i>
<span>الأتمتة</span>
</a>
<div class="sidebar-divider"></div>
<button class="btn btn-demo-sidebar btn-demo">
<i data-lucide="play-circle"></i>
<span>وضع التجربة (Demo)</span>
</button>
</nav>
<div class="user-selector-wrapper">
<label for="user-id-select">المستخدم الحالي:</label>
<select id="user-id-select" class="premium-select">
<option value="">جاري جلب المستخدمين...</option>
</select>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<header class="top-header">
<div class="header-info">
<h1 id="page-title">لوحة الإحصائيات</h1>
<div class="filters glass-card mini-filters">
<div class="filter-group">
<label>الحساب:</label>
<select id="ad-account-select" class="premium-select mini">
<option value="">الحساب الافتراضي</option>
</select>
</div>
<div class="filter-group">
<label>من:</label>
<input type="text" id="date-start" class="premium-input mini date-picker-trigger" placeholder="اختر تاريخ البداية">
</div>
<div class="filter-group">
<label>إلى:</label>
<input type="text" id="date-end" class="premium-input mini date-picker-trigger" placeholder="اختر تاريخ النهاية">
</div>
</div>
</div>
<div class="header-actions">
<button class="btn btn-primary" id="refresh-data">
<i data-lucide="refresh-cw"></i>
تحديث البيانات
</button>
</div>
</header>
<section id="content-area">
<!-- Dashboard Page -->
<div id="dashboard-page" class="page-content">
<div class="platform-filters" id="platform-filter-bar">
<button class="platform-btn active" data-platform="all">الكل</button>
<button class="platform-btn" data-platform="meta">
<i data-lucide="facebook"></i> Facebook
</button>
<button class="platform-btn" data-platform="instagram">
<i data-lucide="instagram"></i> Instagram
</button>
<button class="platform-btn" data-platform="google">
<i data-lucide="search"></i> Google
</button>
<button class="platform-btn" data-platform="tiktok">
<i data-lucide="music-2"></i> تيك توك
</button>
</div>
<div class="stats-grid" id="kpi-cards">
<!-- KPI Cards populated by JS -->
</div>
<div class="chart-section glass-card">
<h3>أداء الحملات الإعلانية</h3>
<canvas id="campaign-chart"></canvas>
</div>
<div class="table-section glass-card">
<h3>تفاصيل الحملات والتوصيات</h3>
<div class="table-wrapper">
<table id="campaign-table">
<thead>
<tr>
<th>المنصة</th>
<th>اسم الحملة</th>
<th>الحالة</th>
<th>الظهور</th>
<th>النقرات</th>
<th>الإنفاق</th>
<th>توصية الذكاء الاصطناعي</th>
<th>إجراء</th>
</tr>
</thead>
<tbody>
<!-- Rows populated by JS -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Connect Account Page -->
<div id="connect-page" class="page-content hidden">
<div class="form-container glass-card">
<h2>ربط حساب إعلاني جديد</h2>
<p class="subtitle">قم بربط حساب ميتـا الخاص بك لبدء التحليل والأتمتة.</p>
<div class="oauth-section" style="margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 1rem;">
<button class="btn btn-primary btn-large w-full" id="btn-oauth-meta" style="background: #1877F2; border: none;">
<i data-lucide="facebook"></i>
الربط التلقائي عبر ميتـا (Facebook & Instagram)
</button>
<button class="btn btn-primary btn-large w-full" id="btn-oauth-google" style="background: #4285F4; border: none;">
<i data-lucide="search"></i>
الربط التلقائي عبر جوجل (Google Ads)
</button>
<button class="btn btn-primary btn-large w-full" id="btn-oauth-tiktok" style="background: #000000; border: none;">
<i data-lucide="music-2"></i>
الربط التلقائي عبر تيك توك (TikTok Ads)
</button>
</div>
<div style="text-align: center; margin-bottom: 1.5rem;">
<span style="background: var(--bg-card); padding: 0 1rem; color: var(--text-dim); font-size: 0.9rem;">أو الربط اليدوي</span>
</div>
<div class="form-group">
<label>اسم الحساب</label>
<input type="text" id="acc-name" placeholder="مثلاً: متجري الإلكتروني">
</div>
<div class="form-group">
<label>Ad Account ID</label>
<input type="text" id="acc-id" placeholder="act_123456789">
</div>
<div class="form-group">
<label>Access Token</label>
<textarea id="acc-token" rows="4" placeholder="EAAY..."></textarea>
</div>
<button class="btn btn-success btn-large" id="btn-connect-meta" style="width: 100%;">
<i data-lucide="link-2"></i>
حفظ الربط اليدوي
</button>
</div>
</div>
<!-- AI Lab Page -->
<div id="ai-lab-page" class="page-content hidden">
<button class="btn btn-secondary mb-3" id="btn-back-to-dashboard" style="display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.25rem;">
<i data-lucide="arrow-right" style="width: 20px; height: 20px;"></i>
<span style="font-weight: 600;">العودة للوحة الإحصائيات</span>
</button>
<div class="ai-grid">
<div class="ai-input-section glass-card">
<h2>تحليل التصميم بالذكاء الاصطناعي</h2>
<p>أدخل رابط صورة الإعلان وسيقوم Gemini بتحليله بصرياً.</p>
<div class="form-group">
<label>رابط الصورة</label>
<input type="text" id="ai-image-url" placeholder="https://example.com/ad-image.jpg">
</div>
<div class="form-group">
<label>النص الإعلاني (اختياري)</label>
<textarea id="ai-copy" rows="3"></textarea>
</div>
<button class="btn btn-sparkle" id="btn-analyze-visual">
<i data-lucide="cpu"></i>
تحليل التصميم بالذكاء الاصطناعي
</button>
</div>
<div class="ai-output-section glass-card" id="ai-result-box">
<div class="placeholder-text">
<i data-lucide="brain-circuit"></i>
التقرير سيظهر هنا بعد التحليل
</div>
</div>
</div>
</div>
<!-- Automation Page -->
<div id="automation-page" class="page-content hidden">
<div class="glass-card">
<h3>إنشاء قاعدة أتمتة جديدة</h3>
<p class="subtitle" style="margin-bottom: 2rem;">تحكم في حملاتك تلقائياً بناءً على الأداء.</p>
<div class="automation-grid" style="display: grid; grid-template-columns: 1fr 1.5fr; gap: 2rem;">
<div class="rule-builder-section">
<form id="rule-form">
<div class="form-group">
<label>اسم القاعدة</label>
<input type="text" id="rule-name" placeholder="مثلاً: إيقاف الإعلانات الضعيفة" required>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div class="form-group">
<label>المؤشر</label>
<select id="rule-metric" class="premium-select">
<option value="spend">الإنفاق</option>
<option value="ctr">نسبة النقر (CTR)</option>
<option value="cpc">تكلفة النقرة (CPC)</option>
<option value="roas">العائد (ROAS)</option>
</select>
</div>
<div class="form-group">
<label>الشرط</label>
<div style="display: flex; gap: 5px;">
<select id="rule-operator" class="premium-select" style="width: 70px;">
<option value=">">&gt;</option>
<option value="<">&lt;</option>
<option value="=">=</option>
</select>
<input type="number" id="rule-value" step="0.1" placeholder="0.0" required>
</div>
</div>
</div>
<div class="form-group">
<label>الإجراء (Action)</label>
<select id="rule-action" class="premium-select">
<option value="notify">تنبيه فقط (Notify)</option>
<option value="pause" disabled title="Pro Feature">إيقاف الحملة (Pause) - Pro</option>
</select>
</div>
<button type="submit" class="btn btn-sparkle w-full">حفظ وتفعيل القاعدة</button>
</form>
</div>
<div class="rules-list-section">
<div class="table-wrapper">
<table id="rules-table">
<thead>
<tr>
<th>القاعدة</th>
<th>الشرط</th>
<th>الإجراء</th>
<th>الحالة</th>
<th></th>
</tr>
</thead>
<tbody id="rules-list-body">
<tr id="rules-empty-state">
<td colspan="5" style="text-align: center; color: var(--text-dim); padding: 2rem;">لا توجد قواعد نشطة حالياً.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- Scripts -->
<script src="src/api.js" type="module"></script>
<script src="src/main.js" type="module"></script>
<script>
// Init icons after page load
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
});
</script>
</body>
</html>

933
frontend/package-lock.json generated Normal file
View File

@@ -0,0 +1,933 @@
{
"name": "frontend",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"version": "0.0.0",
"license": "ISC",
"dependencies": {
"serve-handler": "^6.1.7"
},
"devDependencies": {
"vite": "^8.0.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true,
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true,
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@oxc-project/runtime": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
"dev": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
"integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
"integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
"integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
"integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
"integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
"integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
"integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
"integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
"cpu": [
"wasm32"
],
"dev": true,
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
"integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
"integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
"dev": true
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"dependencies": {
"mime-db": "~1.33.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="
},
"node_modules/path-to-regexp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.9",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
"dev": true,
"dependencies": {
"@oxc-project/types": "=0.115.0",
"@rolldown/pluginutils": "1.0.0-rc.9"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.9",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
"@rolldown/binding-darwin-x64": "1.0.0-rc.9",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
}
},
"node_modules/serve-handler": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz",
"integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.5",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"optional": true
},
"node_modules/vite": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
"dev": true,
"dependencies": {
"@oxc-project/runtime": "0.115.0",
"lightningcss": "^1.32.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.9",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.0.0-alpha.31",
"esbuild": "^0.27.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}

23
frontend/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node serv.js",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.0.0"
},
"dependencies": {
"serve-handler": "^6.1.7"
},
"main": "serv.js",
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
frontend/public/icons.svg Normal file
View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

12
frontend/serv.js Normal file
View File

@@ -0,0 +1,12 @@
import http from 'http';
import handler from 'serve-handler';
const server = http.createServer((request, response) => {
return handler(request, response, {
public: '.'
});
});
server.listen(5001, () => {
console.log('Frontend is running at http://localhost:5001');
});

113
frontend/src/api.js Normal file
View File

@@ -0,0 +1,113 @@
import { auth } from './auth.js';
export const BASE_URL = 'http://localhost:3001/api';
/**
* API Wrapper for SaaS Meta Backend
*/
export const api = {
// Current User State
getCurrentUserId() {
return auth.getUserId() || '';
},
async request(path, options = {}) {
const response = await fetch(`${BASE_URL}${path}`, options);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Network error' }));
throw { status: response.status, ...error };
}
return response.json();
},
// 1. Users
async getAllUsers() {
return this.request('/users');
},
async login(email) {
return this.request('/users/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
},
// 2. Meta Ads
async connectMeta(data) {
const userId = this.getCurrentUserId();
return this.request('/meta/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, userId }) // adAccountId inside data
});
},
async getConnectedAccounts() {
const userId = this.getCurrentUserId();
return this.request('/meta/accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId })
});
},
async getInsights(params = {}) {
const userId = this.getCurrentUserId();
return this.request('/meta/insights', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId
},
body: JSON.stringify(params)
});
},
// 3. AI Analysis
async analyzeVisual(imageUrl, copy, metrics = null, campaignId = null) {
const userId = this.getCurrentUserId();
return this.request('/analyze/visual', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId
},
body: JSON.stringify({ imageUrl, adCopy: copy, metrics, campaignId })
});
},
// 4. Sample Data for Demo Mode
async getSampleData(params = {}) {
const userId = this.getCurrentUserId();
return this.request('/analyze/sample', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId
},
body: JSON.stringify(params)
});
},
// 5. Automation Rules
async getRules() {
const userId = this.getCurrentUserId();
return this.request(`/automation/rules/${userId}`);
},
async createRule(ruleData) {
const userId = this.getCurrentUserId();
return this.request('/automation/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...ruleData, userId })
});
},
async deleteRule(ruleId) {
return this.request(`/automation/rules/${ruleId}`, {
method: 'DELETE'
});
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="32" height="32" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"/><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"/></svg>

After

Width:  |  Height:  |  Size: 863 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

36
frontend/src/auth.js Normal file
View File

@@ -0,0 +1,36 @@
/**
* Authentication Helper
* هيلبر المصادقة
*/
export const auth = {
// Current User Key in LocalStorage
STORAGE_KEY: 'saas_user',
// Save user info to local storage
login(user) {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(user));
return user;
},
// Remove user info (Logout)
logout() {
localStorage.removeItem(this.STORAGE_KEY);
window.location.reload();
},
// Get current user info
getUser() {
const user = localStorage.getItem(this.STORAGE_KEY);
return user ? JSON.parse(user) : null;
},
// Check if user is logged in
isAuthenticated() {
return this.getUser() !== null;
},
// Get User ID for API headers
getUserId() {
return this.getUser()?.id || '';
}
};

9
frontend/src/counter.js Normal file
View File

@@ -0,0 +1,9 @@
export function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `Count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

812
frontend/src/main.js Normal file
View File

@@ -0,0 +1,812 @@
import { api, BASE_URL } from './api.js';
import { auth } from './auth.js';
console.log('--- main.js Version 2.1 Loaded ---');
// Application State
const state = {
activePage: 'dashboard',
users: [],
campaigns: [],
chart: null,
activeMetrics: null,
activeCampaignId: null
};
// --- Initialization ---
async function init() {
initDates();
setupNavigation();
setupEventListeners();
setupAuthEvents();
setupAutomationEvents();
if (auth.isAuthenticated()) {
showApp();
} else {
showLogin();
}
}
// Modules run after DOM is ready by default
init();
function initDates() {
// Standard initialization with Flatpickr
const commonConfig = {
dateFormat: "Y-m-d",
theme: "dark",
locale: "ar" // Flatpickr will look for 'ar' in the included locale script
};
flatpickr("#date-start", {
...commonConfig,
defaultDate: "2025-11-01"
});
flatpickr("#date-end", {
...commonConfig,
defaultDate: "2026-03-25"
});
}
async function loadInitialData() {
try {
// 1. Fetch Users
const users = await api.getAllUsers().catch(err => {
console.error('Failed to fetch users:', err);
return [];
});
state.users = users;
renderUserSelector();
// 2. Load Accounts and Refresh if we have a user
if (state.users.length > 0) {
await loadAccounts();
await refreshDashboard();
} else {
console.warn('No users found in system');
showErrorState({ message: 'لا يوجد مستخدمين متاحين حالياً.' });
}
} catch (error) {
console.error('Failed to load initial data:', error);
}
}
async function loadAccounts() {
const select = document.getElementById('ad-account-select');
if (!select) return;
try {
const accounts = await api.getConnectedAccounts().catch(() => []);
select.innerHTML = '<option value="">الحساب الافتراضي (.env)</option>' +
accounts.map(acc => `<option value="${acc.externalAdAccountId}">${acc.name} (${acc.externalAdAccountId})</option>`).join('');
} catch (err) {
console.error('Failed to load accounts:', err);
}
}
// --- UI Rendering ---
function renderUserSelector() {
const userInfo = document.getElementById('user-info-display');
if (!userInfo) return;
const user = auth.getUser();
if (user) {
userInfo.innerHTML = `
<div class="user-badge glass-card" style="padding: 10px; border-radius: 12px; margin-top: auto;">
<div style="font-size: 0.8rem; color: var(--text-secondary);">مرحباً،</div>
<div style="font-weight: 600; font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis;">${user.email}</div>
<div style="font-size: 0.75rem; color: var(--accent-primary); margin-top: 4px;">المستوى: ${user.subscriptionTier.toUpperCase()}</div>
<button id="logout-btn" class="btn btn-demo-sidebar" style="margin-top: 10px; width: 100%; font-size: 0.75rem; background: rgba(239, 68, 68, 0.1); color: var(--danger); border-color: rgba(239, 68, 68, 0.3);">
<i data-lucide="log-out"></i> تسجيل الخروج
</button>
</div>
`;
document.getElementById('logout-btn')?.addEventListener('click', () => auth.logout());
lucide.createIcons();
}
}
function showLogin() {
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-app').classList.add('hidden');
}
function showApp() {
document.getElementById('login-overlay').classList.add('hidden');
document.getElementById('main-app').classList.remove('hidden');
loadInitialData();
}
function setupAuthEvents() {
const loginForm = document.getElementById('login-form');
const loginError = document.getElementById('login-error');
loginForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('login-email').value;
const submitBtn = loginForm.querySelector('button');
try {
submitBtn.disabled = true;
submitBtn.innerHTML = 'جاري التحقق...';
loginError.classList.add('hidden');
const user = await api.login(email);
auth.login(user);
showApp();
} catch (err) {
loginError.textContent = err.message || 'فشل تسجيل الدخول';
loginError.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = 'دخول للمنصة';
}
});
document.getElementById('btn-google-login')?.addEventListener('click', () => {
alert('تسجيل الدخول عبر Google سيتوفر قريباً بعد إعداد مفاتيح API الخاصة بك.');
// In real app: window.location.href = `${BASE_URL}/auth/google/app-login`;
});
}
async function refreshDashboard(demo = false) {
const dashboard = document.getElementById('dashboard-page');
if (dashboard.classList.contains('hidden')) return;
const refreshBtn = document.getElementById('refresh-data');
if (refreshBtn) refreshBtn.innerHTML = '<i data-lucide="refresh-cw" class="spin"></i> جاري التحميل...';
const params = {
dateStart: document.getElementById('date-start').value,
dateEnd: document.getElementById('date-end').value,
adAccountId: document.getElementById('ad-account-select').value || undefined
};
try {
let data;
if (demo) {
data = await api.getSampleData(params);
} else {
data = await api.getInsights(params);
}
if (data && Array.isArray(data)) {
state.campaigns = data;
renderDynamicDashboard(); // New function for premium UI
hideErrorState();
}
} catch (err) {
console.error('Dashboard refresh failed:', err);
showErrorState(err);
} finally {
if (refreshBtn) {
refreshBtn.innerHTML = '<i data-lucide="refresh-cw"></i> تحديث البيانات';
lucide.createIcons();
}
}
}
function renderDynamicDashboard() {
const activeFilter = document.querySelector('.platform-btn.active')?.dataset.platform || 'all';
let filteredData = [...state.campaigns];
if (activeFilter !== 'all') {
if (activeFilter === 'instagram') {
filteredData = filteredData.filter(c => c.platform === 'instagram');
} else if (activeFilter === 'meta') {
filteredData = filteredData.filter(c => c.source === 'meta' && c.platform !== 'instagram');
} else {
filteredData = filteredData.filter(c => c.source === activeFilter);
}
}
// Sort: ACTIVE/ENABLED first | ترتيب: النشط أولاً
filteredData.sort((a, b) => {
const aActive = a.status === 'ACTIVE' || a.status === 'ENABLED';
const bActive = b.status === 'ACTIVE' || b.status === 'ENABLED';
if (aActive && !bActive) return -1;
if (!aActive && bActive) return 1;
return 0;
});
populatePremiumKPIs(filteredData);
populatePremiumTable(filteredData);
renderChart(filteredData);
}
function populatePremiumKPIs(data) {
const container = document.getElementById('kpi-cards');
if (!container) return;
const stats = {
impressions: data.reduce((sum, c) => sum + (c.impressions || 0), 0),
clicks: data.reduce((sum, c) => sum + (c.clicks || 0), 0),
spend: data.reduce((sum, c) => sum + (c.spend || 0), 0),
ctr: data.length > 0 ? (data.reduce((sum, c) => sum + (c.ctr || 0), 0) / data.length) : 0
};
const cards = [
{ label: 'إجمالي الظهور', value: stats.impressions.toLocaleString(), icon: 'eye', color: 'accent' },
{ label: 'النقرات المستهدفة', value: stats.clicks.toLocaleString(), icon: 'mouse-pointer-2', color: 'success' },
{ label: 'الإنفاق الذكي', value: `$${stats.spend.toLocaleString()}`, icon: 'dollar-sign', color: 'warning' },
{ label: 'متوسط الأداء (CTR)', value: `${stats.ctr.toFixed(2)}%`, icon: 'zap', color: 'danger' }
];
container.innerHTML = cards.map(c => `
<div class="stat-card glass-card">
<div class="stat-icon-wrapper" style="color: var(--${c.color})">
<i data-lucide="${c.icon}"></i>
</div>
<span class="stat-label">${c.label}</span>
<div class="stat-value">${c.value}</div>
</div>
`).join('');
lucide.createIcons();
}
function populatePremiumTable(data) {
const tbody = document.querySelector('#campaign-table tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center; padding: 3rem; opacity: 0.5;">لا توجد حملات لهذه المنصة حالياً</td></tr>';
return;
}
tbody.innerHTML = data.map(c => {
const rec = getAIRecommendation(c);
const statusClass = (c.status === 'ACTIVE' || c.status === 'ENABLED') ? 'status-active' : 'status-paused';
const statusText = (c.status === 'ACTIVE' || c.status === 'ENABLED') ? 'نشط' : (c.status === 'PAUSED' ? 'متوقف' : (c.status || 'غير معروف'));
return `
<tr>
<td>
<div class="platform-icon" style="color: var(--color-${c.platform === 'instagram' ? 'instagram' : c.source})">
<i data-lucide="${c.platform === 'instagram' ? 'instagram' : (c.source === 'meta' ? 'facebook' : (c.source === 'google' ? 'search' : 'music-2'))}"></i>
</div>
</td>
<td style="font-weight: 600;">${c.campaignName || 'Unnamed'}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${formatCompact(c.impressions || 0)}</td>
<td>${formatCompact(c.clicks || 0)}</td>
<td>$${(c.spend || 0).toLocaleString()}</td>
<td>
<span class="ai-status ${rec.type}">${rec.text}</span>
</td>
<td>
<button class="btn btn-analyze-row" onclick="analyzeCampaign('${c.campaignId}')">
<i data-lucide="brain"></i> تحليل
</button>
</td>
</tr>
`;
}).join('');
lucide.createIcons();
}
function getAIRecommendation(c) {
if (c.aiRecommendation) {
return { text: c.aiRecommendation.substring(0, 50) + '...', type: 'excellent' };
}
if (c.ctr > 2.5) return { text: 'أداء مذهل - استمر', type: 'excellent' };
if (c.spend > 100 && c.clicks < 10) return { text: 'إنفاق عالي - راقب', type: 'danger' };
if (c.ctr < 1.0) return { text: 'تحسين العرض مطلوب', type: 'warning' };
return { text: 'أداء مستقر', type: 'excellent' };
}
function formatCompact(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num;
}
window.analyzeCampaign = (id) => {
console.log('analyzeCampaign triggered for ID:', id);
if (!state.campaigns || state.campaigns.length === 0) {
console.warn('analyzeCampaign aborted: state.campaigns is empty.');
return;
}
const campaign = state.campaigns.find(c => String(c.campaignId) === String(id));
if (!campaign) {
console.error('analyzeCampaign: Campaign not found for ID:', id);
console.table(state.campaigns.map(c => ({ id: c.campaignId, name: c.campaignName })));
return;
}
console.log('Found campaign:', campaign.campaignName);
// Clear previous AI state before switching
try {
clearAILab();
} catch (e) {
console.error('Error in clearAILab:', e);
}
state.activeCampaignId = id;
let imageUrl = campaign.imageUrl || '';
if (imageUrl && !imageUrl.startsWith('http')) {
const serverBase = BASE_URL.replace('/api', '');
imageUrl = serverBase + imageUrl;
}
// Switch to AI Lab and pre-fill context
switchPage('ai-lab');
document.getElementById('ai-image-url').value = imageUrl;
// Use real ad copy if available, otherwise fallback to stats
// استخدام نص الإعلان الحقيقي إذا وجد، وإلا نستخدم الإحصائيات
const content = campaign.adCopy || `تحليل الحملة: ${campaign.campaignName}\nالمنصة: ${campaign.source}\nالظهور: ${formatCompact(campaign.impressions || 0)}\nالإنفاق: $${(campaign.spend || 0).toLocaleString()}`;
document.getElementById('ai-copy').value = content;
// Store metrics for the analysis button | تخزين المقاييس لزر التحليل
state.activeMetrics = {
impressions: campaign.impressions,
clicks: campaign.clicks,
spend: campaign.spend,
ctr: campaign.ctr,
cpc: campaign.cpc,
cpm: campaign.cpm,
conversions: campaign.conversions,
costPerResult: campaign.costPerResult,
objective: campaign.objective,
reach: campaign.reach,
frequency: campaign.frequency
};
console.log('Campaign context set for AI Lab:', state.activeMetrics);
};
function renderChart(data) {
const ctx = document.getElementById('campaign-chart').getContext('2d');
if (state.chart) {
state.chart.destroy();
}
state.chart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(c => c.campaignName),
datasets: [
{
label: 'الإنفاق (USD)',
data: data.map(c => c.spend),
backgroundColor: 'rgba(59, 130, 246, 0.5)',
borderColor: '#3b82f6',
borderWidth: 1
},
{
label: 'النقرات',
data: data.map(c => c.clicks),
backgroundColor: 'rgba(16, 185, 129, 0.5)',
borderColor: '#10b981',
borderWidth: 1
}
]
},
options: {
responsive: true,
plugins: {
legend: { labels: { color: '#94a3b8' } }
},
scales: {
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } },
x: { grid: { display: false }, ticks: { color: '#94a3b8' } }
}
}
});
}
// --- Navigation ---
function setupNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const page = item.getAttribute('data-page');
switchPage(page);
});
});
}
function switchPage(pageId) {
console.log('Switching to page:', pageId);
const targetNav = document.querySelector(`[data-page="${pageId}"]`);
const targetPage = document.getElementById(`${pageId}-page`);
if (!targetNav || !targetPage) {
console.error(`switchPage Error: Element not found for pageId: ${pageId}`, { targetNav, targetPage });
return;
}
// UI update
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
targetNav.classList.add('active');
document.querySelectorAll('.page-content').forEach(p => p.classList.add('hidden'));
targetPage.classList.remove('hidden');
document.getElementById('page-title').textContent = targetNav.querySelector('span').textContent;
state.activePage = pageId;
if (pageId === 'dashboard') refreshDashboard();
if (pageId === 'automation') refreshRules();
// Re-init icons for the new page
if (window.lucide) lucide.createIcons();
}
// --- Event Listeners ---
function setupEventListeners() {
document.getElementById('refresh-data')?.addEventListener('click', () => refreshDashboard(false));
// Demo Mode Buttons
document.querySelectorAll('.btn-demo').forEach(btn => {
btn.onclick = () => refreshDashboard(true);
});
// Platform Filters
document.querySelectorAll('.platform-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.platform-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderDynamicDashboard();
});
});
document.getElementById('btn-connect-meta')?.addEventListener('click', async () => {
const data = {
name: document.getElementById('acc-name').value,
adAccountId: document.getElementById('acc-id').value, // Fixed from externalAdAccountId
accessToken: document.getElementById('acc-token').value,
platform: 'meta'
};
try {
await api.connectMeta(data);
alert('تم ربط الحساب بنجاح!');
switchPage('dashboard');
} catch (err) {
console.error(err);
alert(`فشل الربط: ${err.message || 'خطأ غير معروف'}`);
}
});
document.getElementById('btn-oauth-meta')?.addEventListener('click', () => {
const userId = api.getCurrentUserId();
if (!userId) {
alert('يرجى اختيار مستخدم أولاً');
return;
}
window.location.href = `${BASE_URL}/auth/meta?userId=${userId}`;
});
document.getElementById('btn-oauth-google')?.addEventListener('click', () => {
const userId = api.getCurrentUserId();
if (!userId) {
alert('يرجى اختيار مستخدم أولاً');
return;
}
window.location.href = `${BASE_URL}/auth/google?userId=${userId}`;
});
document.getElementById('btn-oauth-tiktok')?.addEventListener('click', () => {
const userId = api.getCurrentUserId();
if (!userId) {
alert('يرجى اختيار مستخدم أولاً');
return;
}
window.location.href = `${BASE_URL}/auth/tiktok?userId=${userId}`;
});
} // Correct closing brace for setupEventListeners
// --- AI Lab Functions and Listeners ---
// 1. دالة التنسيق المخصصة (Zero-Dependency)
function formatGeminiText(text) {
if (!text) return '';
let html = text;
// تنسيق العناوين (Headers)
html = html.replace(/^### (.*$)/gim, '<h3 style="color: #60a5fa; margin-top: 20px; margin-bottom: 10px; font-weight: bold;">$1</h3>');
html = html.replace(/^## (.*$)/gim, '<h2 style="color: #3b82f6; margin-top: 25px; margin-bottom: 15px; border-bottom: 1px solid #334155; padding-bottom: 8px; font-weight: bold;">$1</h2>');
html = html.replace(/^# (.*$)/gim, '<h1 style="color: #fff; margin-bottom: 15px; font-weight: bold;">$1</h1>');
// تنسيق الخط العريض (Bold)
html = html.replace(/\*\*(.*?)\*\*/gim, '<strong style="color: #f8fafc; font-weight: 900;">$1</strong>');
// تنسيق القوائم والنقاط (Lists)
html = html.replace(/^\* (.*$)/gim, '<li style="margin-right: 25px; margin-bottom: 8px;">$1</li>');
html = html.replace(/^- (.*$)/gim, '<li style="margin-right: 25px; margin-bottom: 8px;">$1</li>');
// تنسيق الفواصل (Horizontal Rules)
html = html.replace(/^---/gim, '<hr style="border: 0; border-top: 1px solid #334155; margin: 20px 0;">');
// سطر جديد (Line Breaks) - هذا ما سيمنع تكتل النص ككتلة واحدة
html = html.replace(/\n/gim, '<br>');
return html;
}
// 2. زر التحليل
document.getElementById('btn-analyze-visual')?.addEventListener('click', async () => {
const url = document.getElementById('ai-image-url')?.value;
const copy = document.getElementById('ai-copy')?.value;
const out = document.getElementById('ai-result-box');
const btn = document.getElementById('btn-analyze-visual');
const originalBtnText = btn?.innerHTML || 'تحليل التصميم بالذكاء الاصطناعي';
console.log('--- AI Analysis Clicked ---');
if (!url) {
console.warn('Analysis aborted: No image URL provided.');
if (out) out.innerHTML = '<p style="color:var(--danger); text-align:center; padding:1rem;">الرجاء إدخال رابط الصورة أولاً.</p>';
return;
}
// Disable button and show loader | تعطيل الزر وإظهار مؤشر التحميل
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i data-lucide="refresh-cw" class="spin"></i> جاري التحميل...';
}
if (out) {
out.innerHTML = `
<div style="text-align:center; padding: 2rem;">
<i data-lucide="refresh-cw" class="spin" style="width:40px; height:40px; color:var(--accent);"></i>
<p style="margin-top:1rem; color:var(--text-muted);">جاري تحليل التصميم واستخراج النتائج...</p>
</div>
`;
}
if (window.lucide) lucide.createIcons();
console.log('Requesting analysis with:', { url, copy, metrics: state.activeMetrics, campaignId: state.activeCampaignId });
try {
const res = await api.analyzeVisual(url, copy, state.activeMetrics, state.activeCampaignId);
console.log('Analysis Response Received:', res);
let rawText = res.feedback || res || '';
// 1. إصلاح مشكلة الأسطر (في حال كان السيرفر يرسلها كـ النص الحرفي \n)
rawText = rawText.replace(/\\n/g, '\n');
// 2. استيراد مكتبة marked برمجياً وبشكل ديناميكي (مضمونة 100% في الـ ES Modules)
console.log('Importing marked...');
const module = await import('https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js');
// 3. تحويل النص (العناوين، القوائم، والجداول)
const formattedHTML = module.marked.parse(rawText);
// 4. عرض النتيجة مع CSS داخلي لتجميل الجداول والنصوص
if (out) {
out.innerHTML = `
<style>
.markdown-ai { direction: rtl; text-align: right; line-height: 1.8; color: #cbd5e1; font-size: 15px; padding: 15px; }
.markdown-ai h2, .markdown-ai h3 { color: #60a5fa; margin-top: 25px; border-bottom: 1px solid #334155; padding-bottom: 8px; }
.markdown-ai p { margin-bottom: 15px; }
.markdown-ai strong { color: #fff; background: rgba(255,255,255,0.05); padding: 0 5px; border-radius: 4px; }
.markdown-ai table { width: 100%; border-collapse: collapse; margin-top: 15px; margin-bottom: 20px; }
.markdown-ai th, .markdown-ai td { border: 1px solid #334155; padding: 10px; text-align: right; }
.markdown-ai th { background-color: #1e293b; color: #60a5fa; }
.markdown-ai ul, .markdown-ai ol { padding-right: 25px; margin-bottom: 15px; }
.markdown-ai li { margin-bottom: 8px; }
.markdown-ai pre {
background: #0f172a;
border: 1px solid #334155;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
overflow-x: auto;
direction: ltr;
text-align: left;
position: relative;
}
.markdown-ai code {
font-family: 'Fira Code', 'Courier New', Courier, monospace;
font-size: 14px;
color: #e2e8f0;
}
</style>
<div class="markdown-ai">
${formattedHTML}
</div>
`;
}
} catch (err) {
console.error("Analysis Error:", err);
if (out) {
out.innerHTML = `
<div class="error-placeholder" style="color:var(--danger); padding:2rem; text-align:center;">
<i data-lucide="alert-octagon" style="width:40px; height:40px;"></i>
<p>فشل التحليل. تأكد من الرابط أو حاول لاحقاً.</p>
</div>
`;
}
} finally {
// Re-enable button and restore text | إعادة تفعيل الزر واستعادة النص
if (btn) {
btn.disabled = false;
btn.innerHTML = originalBtnText;
if (window.lucide) lucide.createIcons();
}
}
});
function clearAILab() {
const urlInput = document.getElementById('ai-image-url');
const copyInput = document.getElementById('ai-copy');
const resultBox = document.getElementById('ai-result-box');
state.activeCampaignId = null;
state.activeMetrics = null;
if (urlInput) urlInput.value = '';
if (copyInput) copyInput.value = '';
if (resultBox) {
resultBox.innerHTML = `
<div class="placeholder-text">
<i data-lucide="brain-circuit"></i>
التقرير سيظهر هنا بعد التحليل
</div>
`;
}
if (window.lucide) lucide.createIcons();
}
// Attach back button logic
document.getElementById('btn-back-to-dashboard')?.addEventListener('click', () => {
console.log('Back button clicked! Navigation to dashboard started.');
switchPage('dashboard');
});
function showErrorState(err) {
const tableBody = document.querySelector('#campaign-table tbody');
let message = 'حدث خطأ أثناء جلب البيانات';
if (err.status === 502) {
message = 'انتهت صلاحية الـ Access Token مع ميتـا. يرجى تجديده أو استخدام "وضع التجربة".';
}
tableBody.innerHTML = `
<tr>
<td colspan="6">
<div class="error-placeholder glass-card" style="padding: 2rem; border: 1px dashed var(--danger);">
<i data-lucide="alert-circle" style="width: 48px; height: 48px; color: var(--danger); margin-bottom: 1rem;"></i>
<p style="color: var(--danger); font-weight: bold; margin-bottom: 1rem; font-size: 1.1rem;">${message}</p>
<div style="display: flex; gap: 1rem; justify-content: center;">
<button class="btn btn-primary" onclick="switchPage('connect')">
<i data-lucide="link"></i> ربط حساب جديد (Reconnect)
</button>
<button class="btn btn-demo">
<i data-lucide="play"></i> تشغيل وضع التجربة (Demo Mode)
</button>
</div>
</div>
</td>
</tr>
`;
lucide.createIcons();
}
function hideErrorState() {
// Reset handled by populateTable
}
function checkAuthRedirect() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('auth')) {
const status = urlParams.get('auth');
const platform = urlParams.get('platform');
if (status === 'success') {
alert(`تم ربط حساب ${platform} بنجاح!`);
// Clean URL
window.history.replaceState({}, document.title, window.location.pathname);
} else if (status === 'failed') {
alert('فشل عملية الربط، يرجى المحاولة مرة أخرى.');
window.history.replaceState({}, document.title, window.location.pathname);
switchPage('connect'); // Take them back to try again
}
}
}
// --- Automation Rules ---
async function refreshRules() {
console.log('Refreshing automation rules...');
const tbody = document.getElementById('rules-list-body');
if (!tbody) return;
try {
const rules = await api.getRules();
renderRules(rules);
} catch (err) {
console.error('Failed to fetch rules:', err);
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; color: var(--danger);">فشل تحميل القواعد.</td></tr>`;
}
}
function renderRules(rules) {
const tbody = document.getElementById('rules-list-body');
if (!tbody) return;
if (!rules || rules.length === 0) {
tbody.innerHTML = `<tr id="rules-empty-state"><td colspan="5" style="text-align: center; color: var(--text-dim); padding: 2rem;">لا توجد قواعد نشطة حالياً.</td></tr>`;
return;
}
tbody.innerHTML = rules.map(rule => {
const cond = rule.conditions[0] || { metric: '?', operator: '?', value: '?' };
return `
<tr>
<td><strong>${rule.name}</strong></td>
<td><span style="direction: ltr; display: inline-block;">${cond.metric.toUpperCase()} ${cond.operator} ${cond.value}</span></td>
<td>${rule.action === 'notify' ? 'تنبيه' : 'إيقاف'}</td>
<td><span class="status-badge ${rule.isActive ? '' : 'inactive'}">${rule.isActive ? 'نشط' : 'متوقف'}</span></td>
<td style="text-align: left;">
<button class="btn-delete-rule" onclick="handleDeleteRule('${rule.id}')" title="حذف القاعدة">
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i>
</button>
</td>
</tr>
`;
}).join('');
lucide.createIcons();
}
async function handleDeleteRule(id) {
if (!confirm('هل أنت متأكد من رغبتك في حذف هذه القاعدة؟')) return;
try {
await api.deleteRule(id);
await refreshRules();
} catch (err) {
alert('فشل في حذف القاعدة: ' + (err.message || 'خطأ غير معروف'));
}
}
window.handleDeleteRule = handleDeleteRule;
function setupAutomationEvents() {
const form = document.getElementById('rule-form');
if (!form) return;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = form.querySelector('button');
const ruleData = {
name: document.getElementById('rule-name').value,
platform: 'meta',
targetId: 'all',
action: document.getElementById('rule-action').value,
conditions: [
{
metric: document.getElementById('rule-metric').value,
operator: document.getElementById('rule-operator').value,
value: parseFloat(document.getElementById('rule-value').value)
}
],
isActive: true
};
try {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> جاري الحفظ...';
lucide.createIcons();
await api.createRule(ruleData);
form.reset();
await refreshRules();
alert('تم إنشاء وتفعيل القاعدة بنجاح! 🚀');
} catch (err) {
console.error('Create rule failed:', err);
alert('فشل حفظ القاعدة: ' + (err.message || 'تأكد من اختيار مستخدم وارتباط حساب إعلاني'));
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = 'حفظ وتفعيل القاعدة';
lucide.createIcons();
}
});
}

565
frontend/src/style.css Normal file
View File

@@ -0,0 +1,565 @@
:root {
/* Color Palette - Premium Dark Theme */
--bg-main: #0a0c10;
--bg-sidebar: #11141b;
--accent-primary: #3b82f6; /* Modern Blue */
--accent-glow: rgba(59, 130, 246, 0.4);
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
/* Platform Colors */
--color-meta: #0668E1;
--color-google: #4285F4;
--color-tiktok: #EE1D52;
--color-instagram: #E1306C;
/* Animation Speeds */
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Noto Kufi Arabic', sans-serif;
background-color: var(--bg-main);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
.app-container {
display: flex;
min-height: 100vh;
}
/* Sidebar Styling */
.sidebar {
width: 280px;
background-color: var(--bg-sidebar);
border-left: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
padding: 2rem 1.5rem;
position: sticky;
top: 0;
height: 100vh;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 3rem;
color: var(--accent-primary);
}
.logo-icon {
width: 40px;
height: 40px;
background: var(--accent-primary);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
box-shadow: 0 0 15px var(--accent-glow);
}
.side-nav {
display: flex;
flex-direction: column;
gap: 10px;
flex-grow: 1;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 12px;
text-decoration: none;
color: var(--text-secondary);
transition: var(--transition);
}
.nav-item:hover, .nav-item.active {
background: var(--glass-bg);
color: var(--text-primary);
}
.nav-item.active {
border-right: 3px solid var(--accent-primary);
}
.nav-item i {
width: 20px;
}
.user-selector-wrapper {
margin-top: auto;
padding-top: 2rem;
border-top: 1px solid var(--glass-border);
}
.premium-select {
width: 100%;
background: var(--bg-main);
border: 1px solid var(--glass-border);
color: var(--text-primary);
padding: 10px;
border-radius: 8px;
margin-top: 8px;
outline: none;
}
/* Main Content Area */
.main-content {
flex-grow: 1;
padding: 2.5rem 3.5rem;
background-image:
radial-gradient(circle at 0% 0%, rgba(59, 130, 246, 0.05) 0%, transparent 40%),
radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.05) 0%, transparent 40%);
}
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
h1 {
font-size: 2.2rem;
font-weight: 700;
}
/* Glass Cards */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
transition: var(--transition);
}
.glass-card:hover {
border-color: rgba(59, 130, 246, 0.3);
transform: translateY(-2px);
}
/* Stats Logic */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-bottom: 2rem;
}
.stat-card {
position: relative;
overflow: hidden;
padding: 1.8rem;
border-radius: 28px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.01) 100%);
text-align: right;
}
.stat-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at center, var(--accent-glow) 0%, transparent 70%);
opacity: 0.1;
pointer-events: none;
}
.stat-label {
display: block;
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 8px;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 8px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.85rem;
font-weight: 600;
}
.stat-trend.positive { color: var(--success); }
.stat-trend.negative { color: var(--danger); }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 12px;
border: none;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.btn-primary { background: var(--accent-primary); color: white; }
.btn-primary:hover { background: #2563eb; transform: scale(1.02); }
.btn-sparkle {
background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899);
color: white;
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.4);
}
/* Form Styling */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-secondary);
}
input, textarea {
width: 100%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--glass-border);
color: var(--text-primary);
padding: 12px 16px;
border-radius: 12px;
outline: none;
transition: var(--transition);
}
input:focus, textarea:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Table Styling */
.table-wrapper {
overflow-x: auto;
margin-top: 1.5rem;
}
table {
width: 100%;
border-collapse: collapse;
text-align: right;
}
th {
padding: 12px 16px;
color: var(--text-secondary);
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
border-bottom: 1px solid var(--glass-border);
}
td {
padding: 16px;
border-bottom: 1px solid var(--glass-border);
}
/* Helpers */
.hidden { display: none !important; }
/* Micro-animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.page-content {
animation: fadeIn 0.4s ease-out forwards;
}
/* Filter Controls */
.header-info {
display: flex;
flex-direction: column;
gap: 1rem;
}
.filters {
display: flex;
gap: 1.5rem;
padding: 0.8rem 1.5rem;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
color: var(--text-secondary);
}
.mini {
padding: 6px 10px !important;
font-size: 0.85rem !important;
margin-top: 0 !important;
}
.premium-input {
background: var(--bg-main);
border: 1px solid var(--glass-border);
color: var(--text-primary);
border-radius: 8px;
outline: none;
}
/* Demo Mode Styling */
.sidebar-divider {
height: 1px;
background: var(--glass-border);
margin: 1.5rem 0;
}
.btn-demo-sidebar {
width: 100%;
justify-content: center;
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 12px;
}
.btn-demo-sidebar:hover {
background: rgba(16, 185, 129, 0.2);
transform: translateY(-2px);
}
.error-placeholder {
padding: 3rem;
text-align: center;
}
.loader.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.badge.active {
background: rgba(16, 185, 129, 0.2);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.3);
padding: 2px 8px;
border-radius: 6px;
font-size: 0.75rem;
}
/* AI Labels */
.ai-status {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.ai-status.excellent { background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); }
.ai-status.warning { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.3); }
.ai-status.danger { background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); }
/* UI Badges */
.status-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
display: inline-block;
}
.status-active {
background: rgba(16, 185, 129, 0.15);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-paused {
background: rgba(148, 163, 184, 0.15);
color: var(--text-secondary);
border: 1px solid rgba(148, 163, 184, 0.3);
}
/* Platform Icons Wrapper */
.platform-icon {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
margin-left: 8px;
}
/* Platform Filter Bar */
.platform-filters {
display: flex;
gap: 10px;
margin-bottom: 1.5rem;
}
.platform-btn {
padding: 8px 16px;
border-radius: 100px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
}
.platform-btn.active {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
box-shadow: 0 4px 12px var(--accent-glow);
}
.btn-analyze-row {
padding: 4px 8px;
font-size: 0.75rem;
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
border: 1px solid rgba(168, 85, 247, 0.3);
}
.btn-analyze-row:hover {
background: #a855f7;
color: white;
}
/* Login Overlay */
.login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-main);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background-image:
radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
}
.login-card {
width: 100%;
max-width: 420px;
padding: 3rem !important;
animation: fadeIn 0.6s ease-out;
}
.error-text {
font-size: 0.85rem;
padding: 10px;
background: rgba(239, 68, 68, 0.1);
border-radius: 8px;
border: 1px solid rgba(239, 68, 68, 0.2);
}
/* Automation Rules UI */
.automation-grid {
margin-top: 1rem;
}
.premium-select {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-main);
padding: 10px;
border-radius: 8px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.premium-select:focus {
border-color: var(--accent-primary);
}
.rules-list-section table {
width: 100%;
border-collapse: collapse;
}
.rules-list-section th {
text-align: right;
font-size: 0.8rem;
color: var(--text-secondary);
padding: 12px;
border-bottom: 2px solid var(--border);
}
.rules-list-section td {
padding: 12px;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
}
.status-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.status-badge.inactive {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.btn-delete-rule {
color: var(--danger);
background: transparent;
border: none;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
}
.btn-delete-rule:hover {
opacity: 1;
}