Initial commit from saas-meta
This commit is contained in:
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal 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
348
frontend/index.html
Normal 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=">">></option>
|
||||
<option value="<"><</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
933
frontend/package-lock.json
generated
Normal 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
23
frontend/package.json
Normal 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": ""
|
||||
}
|
||||
1
frontend/public/favicon.svg
Normal file
1
frontend/public/favicon.svg
Normal file
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
24
frontend/public/icons.svg
Normal 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
12
frontend/serv.js
Normal 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
113
frontend/src/api.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
BIN
frontend/src/assets/hero.png
Normal file
BIN
frontend/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
1
frontend/src/assets/javascript.svg
Normal file
1
frontend/src/assets/javascript.svg
Normal 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 |
1
frontend/src/assets/vite.svg
Normal file
1
frontend/src/assets/vite.svg
Normal file
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
36
frontend/src/auth.js
Normal 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
9
frontend/src/counter.js
Normal 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
812
frontend/src/main.js
Normal 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
565
frontend/src/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user