v1.8.17: unified header/footer via components.js — all pages use same header, auth, branding
This commit is contained in:
Binary file not shown.
@@ -35,5 +35,6 @@ p.desc{color:#dce1fb;font-size:15px;line-height:1.7;margin:0 0 32px}
|
||||
<span style="margin:0 12px">© 2026 AfakZaid</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+141
-68
@@ -1,63 +1,67 @@
|
||||
|
||||
// ── Sticky footer ─────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
const b = document.body;
|
||||
b.style.minHeight = '100vh';
|
||||
b.style.display = 'flex';
|
||||
b.style.flexDirection = 'column';
|
||||
const main = document.querySelector('main');
|
||||
if (main) main.style.flex = '1';
|
||||
})();
|
||||
|
||||
|
||||
// ── Favicon ───────────────────────────────────────────────────────────────
|
||||
(function() {
|
||||
if (!document.querySelector('link[rel="icon"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'icon'; l.type = 'image/x-icon'; l.href = '/favicon.ico';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
if (!document.querySelector('link[rel="apple-touch-icon"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'apple-touch-icon'; l.href = '/apple-touch-icon.png';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Universal Header & Footer ─────────────────────────────────────────────
|
||||
// Include this script in every page. It injects the header and footer,
|
||||
// loads branding, and handles auth nav automatically.
|
||||
// ── AfakZaid RAG — Universal Components ──────────────────────────────────────
|
||||
// Provides: header, footer, branding, auth nav, favicon, scroll behavior
|
||||
// Include on every page: <script src="/components.js"></script>
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Page context detection ─────────────────────────────────────────────
|
||||
const path = window.location.pathname;
|
||||
const isLibrary = path.startsWith('/library');
|
||||
const isIndex = path === '/' || path === '/index.html';
|
||||
const isReader = path.startsWith('/reader');
|
||||
|
||||
// ── Nav links per page ─────────────────────────────────────────────────
|
||||
const NAV_LINKS = {
|
||||
library: [
|
||||
{ href: '/', icon: 'search', label: 'البحث الذكي' },
|
||||
{ href: '#favorites', icon: 'favorite', label: 'المفضلة', onclick: 'window.showFavorites && showFavorites()' },
|
||||
],
|
||||
index: [
|
||||
{ href: '/library', icon: 'library_books', label: 'المكتبة' },
|
||||
],
|
||||
default: [
|
||||
{ href: '/', icon: 'search', label: 'البحث الذكي' },
|
||||
{ href: '/library', icon: 'library_books', label: 'المكتبة' },
|
||||
],
|
||||
};
|
||||
|
||||
const links = isLibrary ? NAV_LINKS.library : isIndex ? NAV_LINKS.index : NAV_LINKS.default;
|
||||
const navLinksHTML = links.map(l => `
|
||||
<a href="${l.href}" ${l.onclick ? `onclick="${l.onclick}"` : ''}
|
||||
class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">${l.icon}</span>
|
||||
<span class="hidden sm:inline">${l.label}</span>
|
||||
</a>`).join('');
|
||||
|
||||
// ── Header HTML ────────────────────────────────────────────────────────
|
||||
const HEADER_HTML = `
|
||||
<header class="sticky top-0 z-20 glass border-b border-primary/20">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-8 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-8 py-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 bg-primary/15 rounded-xl flex items-center justify-center glow-sm">
|
||||
<span class="msym text-primary text-lg">auto_awesome</span>
|
||||
<div class="w-9 h-9 bg-primary/15 rounded-xl flex items-center justify-center glow-sm brand-icon-wrap">
|
||||
<img id="brand-logo" class="hidden rounded-lg object-contain" style="height:40px"/>
|
||||
<span id="brand-icon" class="msym text-primary text-lg">auto_awesome</span>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<h1 class="text-base font-bold text-primary leading-tight" id="brand-name">AfakZaid RAG</h1>
|
||||
<h1 class="text-sm font-bold text-primary leading-tight" id="brand-name">AfakZaid RAG</h1>
|
||||
<p class="text-[10px] text-on-surface-variant/60 uppercase tracking-widest" id="brand-sub">Scholarly Intelligence</p>
|
||||
</div>
|
||||
</a>
|
||||
<div class="h-5 w-px bg-outline-variant/40 hidden sm:block"></div>
|
||||
<a href="/library" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">library_books</span>
|
||||
<span class="hidden sm:inline">المكتبة</span>
|
||||
</a>
|
||||
${navLinksHTML}
|
||||
</div>
|
||||
<nav class="flex items-center gap-2">
|
||||
<nav class="flex items-center gap-2 shrink-0">
|
||||
<div id="nav-auth" class="flex items-center gap-2"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>`;
|
||||
|
||||
// ── Footer HTML ────────────────────────────────────────────────────────
|
||||
const FOOTER_HTML = `
|
||||
<footer class="text-center py-6 mt-8 text-xs text-on-surface-variant border-t border-white/5">
|
||||
<div class="flex items-center justify-center gap-4 flex-wrap">
|
||||
<footer class="text-center py-6 mt-auto text-xs text-on-surface-variant border-t border-white/5">
|
||||
<div class="flex items-center justify-center gap-4 flex-wrap px-4">
|
||||
<a href="/privacy" class="hover:text-primary transition-colors">سياسة الخصوصية</a>
|
||||
<span class="opacity-30">•</span>
|
||||
<a href="/terms" class="hover:text-primary transition-colors">شروط الخدمة</a>
|
||||
@@ -66,52 +70,117 @@ const FOOTER_HTML = `
|
||||
</div>
|
||||
</footer>`;
|
||||
|
||||
// Inject header
|
||||
// ── Inject header ──────────────────────────────────────────────────────
|
||||
if (!isReader) {
|
||||
const headerMount = document.getElementById('universal-header');
|
||||
if (headerMount) headerMount.outerHTML = HEADER_HTML;
|
||||
else document.body.insertAdjacentHTML('afterbegin', HEADER_HTML);
|
||||
else if (!document.querySelector('header')) document.body.insertAdjacentHTML('afterbegin', HEADER_HTML);
|
||||
}
|
||||
|
||||
// Inject footer
|
||||
// ── Inject footer ──────────────────────────────────────────────────────
|
||||
if (!isReader) {
|
||||
const footerMount = document.getElementById('universal-footer');
|
||||
if (footerMount) footerMount.outerHTML = FOOTER_HTML;
|
||||
else document.body.insertAdjacentHTML('beforeend', FOOTER_HTML);
|
||||
}
|
||||
|
||||
// ── Branding ──────────────────────────────────────────────────────────────
|
||||
// ── Favicon ────────────────────────────────────────────────────────────
|
||||
if (!document.querySelector('link[rel="icon"]')) {
|
||||
const l = document.createElement('link');
|
||||
l.rel = 'icon'; l.type = 'image/x-icon'; l.href = '/favicon.ico';
|
||||
document.head.appendChild(l);
|
||||
}
|
||||
|
||||
// ── Branding ───────────────────────────────────────────────────────────
|
||||
fetch('/branding').then(r => r.json()).then(b => {
|
||||
const n = document.getElementById('brand-name');
|
||||
const s = document.getElementById('brand-sub');
|
||||
const f = document.getElementById('footer-brand');
|
||||
if (n && b.app_name) n.textContent = b.app_name;
|
||||
if (s && b.tagline) s.textContent = b.tagline;
|
||||
if (f && b.app_name) f.textContent = '© 2026 ' + b.app_name;
|
||||
// Text
|
||||
const bn = document.getElementById('brand-name');
|
||||
const bs = document.getElementById('brand-sub');
|
||||
const fb = document.getElementById('footer-brand');
|
||||
if (bn && b.site_name) bn.textContent = b.site_name;
|
||||
if (bs && b.site_subtitle) bs.textContent = b.site_subtitle;
|
||||
if (fb && b.site_name) fb.textContent = '© 2026 ' + b.site_name;
|
||||
if (document.title === 'AfakZaid RAG' && b.site_name) document.title = b.site_name;
|
||||
|
||||
// Logo
|
||||
if (b.logo_url) {
|
||||
const logo = document.getElementById('brand-logo');
|
||||
const icon = document.getElementById('brand-icon');
|
||||
if (logo) { logo.src = b.logo_url; logo.style.height = (b.logo_size || 32) + 'px'; logo.classList.remove('hidden'); }
|
||||
if (icon) icon.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Colors
|
||||
if (b.primary_color || b.secondary_color) {
|
||||
const p = b.primary_color || '#4edea3';
|
||||
const s = b.secondary_color || '#95d3ba';
|
||||
const bg = b.bg_color || '#0c1324';
|
||||
const hr = hex => {
|
||||
hex = hex.replace('#', '');
|
||||
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||||
return parseInt(hex.slice(0,2),16)+','+parseInt(hex.slice(2,4),16)+','+parseInt(hex.slice(4,6),16);
|
||||
};
|
||||
const pr = hr(p), sr = hr(s);
|
||||
if (b.bg_image) {
|
||||
document.body.style.backgroundImage = 'url(' + b.bg_image + ')';
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
} else {
|
||||
document.body.style.background = 'radial-gradient(circle at 50% -20%,' + bg + ' 0%,' + bg + ' 100%)';
|
||||
}
|
||||
const old = document.getElementById('_bco'); if (old) old.remove();
|
||||
const st = document.createElement('style'); st.id = '_bco';
|
||||
const rules = [
|
||||
'.text-primary{color:'+p+'!important}',
|
||||
'.bg-primary{background-color:'+p+'!important}',
|
||||
'.border-primary{border-color:'+p+'!important}',
|
||||
'.text-secondary{color:'+s+'!important}',
|
||||
'.bg-secondary{background-color:'+s+'!important}',
|
||||
'.text-tertiary{color:'+s+'!important}',
|
||||
'.hover\\:text-primary:hover{color:'+p+'!important}',
|
||||
'.focus\\:border-primary:focus{border-color:'+p+'!important}',
|
||||
'.glass{border-color:rgba('+pr+',0.12)!important}',
|
||||
'.glass-hover:hover{background:rgba('+pr+',0.06)!important;border-color:rgba('+pr+',0.25)!important}',
|
||||
'.glow{box-shadow:0 0 20px rgba('+pr+',0.15)!important}',
|
||||
'.glow-sm{box-shadow:0 0 10px rgba('+pr+',0.1)!important}',
|
||||
'.filter-chip.active{background:rgba('+pr+',0.12)!important;border-color:rgba('+pr+',0.4)!important;color:'+p+'!important}',
|
||||
'.book-card:hover{box-shadow:0 8px 24px rgba('+pr+',0.1)!important}',
|
||||
'.upload-zone{border-color:rgba('+pr+',0.2)!important}',
|
||||
'.scrollbar::-webkit-scrollbar-thumb{background:rgba('+pr+',0.2)!important}',
|
||||
'.nav-btn.active{background-color:rgba('+pr+',0.1)!important;color:'+p+'!important}',
|
||||
'.bg-primary\\/15{background-color:rgba('+pr+',0.15)!important}',
|
||||
'.bg-primary\\/10{background-color:rgba('+pr+',0.10)!important}',
|
||||
'.bg-primary\\/5{background-color:rgba('+pr+',0.05)!important}',
|
||||
'.border-primary\\/20{border-color:rgba('+pr+',0.20)!important}',
|
||||
'.border-primary\\/30{border-color:rgba('+pr+',0.30)!important}',
|
||||
];
|
||||
st.textContent = rules.join('\n');
|
||||
document.head.appendChild(st);
|
||||
document.documentElement.style.setProperty('--primary', p);
|
||||
document.documentElement.style.setProperty('--secondary', s);
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// ── Auth Nav ──────────────────────────────────────────────────────────────
|
||||
// ── Auth Nav ───────────────────────────────────────────────────────────
|
||||
const userToken = localStorage.getItem('rag_token');
|
||||
|
||||
async function initHeaderAuth() {
|
||||
const nav = document.getElementById('nav-auth');
|
||||
if (!nav) return;
|
||||
|
||||
if (!userToken) {
|
||||
nav.innerHTML = `<a href="/login" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">login</span>
|
||||
<span class="hidden sm:inline">دخول</span>
|
||||
</a>`;
|
||||
nav.innerHTML = `<a href="/login" class="bg-primary text-surface font-bold px-3 py-2 rounded-xl text-xs flex items-center gap-1.5">
|
||||
<span class="msym text-base">login</span><span>دخول</span></a>`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await fetch('/auth/me', { headers: { 'Authorization': 'Bearer ' + userToken } });
|
||||
if (!r.ok) { localStorage.removeItem('rag_token'); initHeaderAuth(); return; }
|
||||
const user = await r.json();
|
||||
const perms = user.permissions || [];
|
||||
const isAdmin = user.role === 'admin';
|
||||
|
||||
nav.innerHTML = `
|
||||
<div class="relative" id="uh-user-wrap">
|
||||
<div class="relative" id="uh-wrap">
|
||||
<button onclick="document.getElementById('uh-drop').classList.toggle('hidden')"
|
||||
class="glass flex items-center gap-2 px-3 py-2 rounded-xl text-xs hover:border-primary/40 transition-colors">
|
||||
class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs transition-colors">
|
||||
<span class="msym text-base text-primary">person</span>
|
||||
<span class="hidden sm:inline max-w-[80px] truncate">${user.name}</span>
|
||||
<span class="msym text-sm text-on-surface-variant">expand_more</span>
|
||||
@@ -119,7 +188,7 @@ async function initHeaderAuth() {
|
||||
<div id="uh-drop" class="hidden absolute right-0 top-full mt-1 glass rounded-xl p-2 min-w-[160px] z-50 shadow-xl">
|
||||
<div class="px-3 py-2 border-b border-outline-variant/30 mb-1">
|
||||
<p class="text-xs font-bold">${user.name}</p>
|
||||
<p class="text-[10px] text-primary">${user.role_display || user.role}</p>
|
||||
<p class="text-[10px] text-primary">${user.role}</p>
|
||||
</div>
|
||||
${isAdmin ? '<a href="/admin" class="flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-on-surface-variant hover:text-primary hover:bg-primary/5"><span class="msym text-sm">admin_panel_settings</span>Admin</a>' : ''}
|
||||
<button onclick="uhLogout()" class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-error hover:bg-red-900/10">
|
||||
@@ -127,21 +196,25 @@ async function initHeaderAuth() {
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const w = document.getElementById('uh-user-wrap');
|
||||
const w = document.getElementById('uh-wrap');
|
||||
if (w && !w.contains(e.target)) document.getElementById('uh-drop')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
window.uhLogout = function () {
|
||||
fetch('/auth/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + userToken } });
|
||||
if (userToken) fetch('/auth/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + userToken } });
|
||||
localStorage.removeItem('rag_token');
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
initHeaderAuth();
|
||||
// ── Flex layout for sticky footer ──────────────────────────────────────
|
||||
document.body.style.minHeight = '100vh';
|
||||
document.body.style.display = 'flex';
|
||||
document.body.style.flexDirection = 'column';
|
||||
const mainEl = document.querySelector('main');
|
||||
if (mainEl) mainEl.style.flex = '1';
|
||||
|
||||
initHeaderAuth();
|
||||
})();
|
||||
|
||||
+3
-141
@@ -251,29 +251,7 @@ header { animation:slideDown 350ms var(--ease-out) both; }
|
||||
<div class="fixed bottom-0 left-0 -z-10 w-72 h-72 bg-tertiary/5 rounded-full blur-[100px] pointer-events-none orb-2"></div>
|
||||
<div class="fixed top-1/2 left-1/2 -z-10 w-64 h-64 bg-secondary/3 rounded-full blur-[140px] pointer-events-none" style="transform:translate(-50%,-50%);animation:orbFloat 16s ease-in-out infinite 4s"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-20 glass border-b border-primary/20">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-8 py-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<div class="w-8 h-8 bg-primary/15 rounded-xl flex items-center justify-center shrink-0 brand-icon-wrap">
|
||||
<span class="msym text-primary text-base">auto_awesome</span>
|
||||
</div>
|
||||
<div class="min-w-0 hidden sm:block">
|
||||
<h1 class="text-sm font-bold text-primary leading-tight truncate" id="brand-name">AfakZaid RAG</h1>
|
||||
<p class="text-[10px] text-on-surface-variant/60 uppercase tracking-widest" id="brand-sub">Scholarly Intelligence</p>
|
||||
</div>
|
||||
<div class="h-5 w-px bg-outline-variant/40 hidden sm:block"></div>
|
||||
<a href="/library" class="glass glass-hover flex items-center gap-1 px-2.5 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">library_books</span>
|
||||
<span class="hidden sm:inline">المكتبة</span>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="flex items-center gap-2 shrink-0">
|
||||
<div id="nav-user" class="shrink-0"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="universal-header"></div>
|
||||
<main class="max-w-6xl mx-auto px-4 lg:px-8 py-8">
|
||||
|
||||
<!-- Stats -->
|
||||
@@ -369,92 +347,6 @@ const API_KEY = 'zRf77E_brSEnA2BUi40uxLxT4uofAk661WMZb-cdv4M';
|
||||
let userToken = localStorage.getItem('rag_token');
|
||||
let currentUser = null;
|
||||
|
||||
// ── Branding ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// -- Branding color override --------------------------------------------------
|
||||
function applyBrandingColors(p, s, bg, bgImg) {
|
||||
p = p || '#4edea3';
|
||||
s = s || '#95d3ba';
|
||||
bg = bg || '#0c1324';
|
||||
const hr = (hex) => {
|
||||
hex = hex.replace('#','');
|
||||
if(hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||||
return parseInt(hex.slice(0,2),16)+','+parseInt(hex.slice(2,4),16)+','+parseInt(hex.slice(4,6),16);
|
||||
};
|
||||
const pr = hr(p), sr = hr(s), bgr = hr(bg);
|
||||
// Background
|
||||
if(bgImg) { document.body.style.backgroundImage='url('+bgImg+')'; document.body.style.backgroundSize='cover'; document.body.style.backgroundPosition='center'; }
|
||||
else { document.body.style.backgroundImage=''; document.body.style.background='radial-gradient(circle at 50% -20%,'+bg+' 0%,'+bg+' 100%)'; }
|
||||
// Remove old style
|
||||
const old = document.getElementById('_bco'); if(old) old.remove();
|
||||
const st = document.createElement('style'); st.id='_bco';
|
||||
st.textContent = [
|
||||
// Primary color — text, bg, border
|
||||
'.text-primary{color:'+p+'!important}',
|
||||
'.bg-primary{background-color:'+p+'!important}',
|
||||
'.border-primary{border-color:'+p+'!important}',
|
||||
'[class*="text-primary"]{color:'+p+'!important}',
|
||||
'[class*="bg-primary\/"]{}',
|
||||
// Secondary
|
||||
'.text-secondary{color:'+s+'!important}',
|
||||
'.bg-secondary{background-color:'+s+'!important}',
|
||||
'.border-secondary{border-color:'+s+'!important}',
|
||||
'.text-tertiary{color:'+s+'!important}',
|
||||
// Hover/focus states
|
||||
'.hover\:text-primary:hover{color:'+p+'!important}',
|
||||
'.hover\:bg-primary\/5:hover{background-color:rgba('+pr+',0.05)!important}',
|
||||
'.focus\:border-primary:focus{border-color:'+p+'!important}',
|
||||
// Active nav
|
||||
'.active.bg-primary\/10{background-color:rgba('+pr+',0.1)!important}',
|
||||
'.active.text-primary{color:'+p+'!important}',
|
||||
'button.active{color:'+p+'!important}',
|
||||
// Glass
|
||||
'.glass{border-color:rgba('+pr+',0.12)!important}',
|
||||
'.glass-hover:hover{background:rgba('+pr+',0.06)!important;border-color:rgba('+pr+',0.25)!important}',
|
||||
// Glows
|
||||
'.glow{box-shadow:0 0 20px rgba('+pr+',0.15)!important}',
|
||||
'.glow-sm{box-shadow:0 0 10px rgba('+pr+',0.1)!important}',
|
||||
'.glass.glow{box-shadow:inset 0 1px 0 rgba(255,255,255,0.06),0 0 20px rgba('+pr+',0.18),0 4px 24px rgba(0,0,0,0.15)!important}',
|
||||
'.glass.glow-sm{box-shadow:inset 0 1px 0 rgba(255,255,255,0.06),0 0 10px rgba('+pr+',0.12)!important}',
|
||||
// Spotlight card gradient
|
||||
'.spotlight-card::before{background:radial-gradient(400px circle at var(--mx,50%) var(--my,50%),rgba('+pr+',0.06),transparent 60%)!important}',
|
||||
// Alpha variants
|
||||
'.bg-primary\/15,.bg-primary\/10,.bg-primary\/5{background-color:rgba('+pr+',0.1)!important}',
|
||||
'.border-primary\/20{border-color:rgba('+pr+',0.2)!important}',
|
||||
'.border-primary\/30{border-color:rgba('+pr+',0.3)!important}',
|
||||
// Upload zone
|
||||
'.upload-zone{border-color:rgba('+pr+',0.2)!important}',
|
||||
'.upload-zone.dragover{border-color:rgba('+pr+',0.6)!important}',
|
||||
// Scrollbar
|
||||
'.scrollbar::-webkit-scrollbar-thumb{background:rgba('+pr+',0.2)!important}',
|
||||
// Input focus
|
||||
'.inp:focus,.inp:focus-within{border-color:'+p+'!important;box-shadow:0 0 0 2px rgba('+pr+',0.1)!important}',
|
||||
// Filter chips active
|
||||
'.filter-chip.active{background:rgba('+pr+',0.12)!important;border-color:rgba('+pr+',0.4)!important;color:'+p+'!important}',
|
||||
// Book card hover
|
||||
'.book-card:hover{box-shadow:0 8px 24px rgba('+pr+',0.1)!important;border-color:rgba('+pr+',0.28)!important}',
|
||||
// Nav active
|
||||
'.nav-btn.active{background-color:rgba('+pr+',0.1)!important;color:'+p+'!important}',
|
||||
// Sidebar border
|
||||
'aside{border-color:rgba('+pr+',0.2)!important}',
|
||||
// CTA button shimmer
|
||||
'.cta-primary::before{background:linear-gradient(90deg,transparent,rgba('+pr+',0.1),transparent)!important}',
|
||||
// Shadow glow on hover
|
||||
'[class*="hover:shadow"]:hover{--tw-shadow-color:rgba('+pr+',0.4)!important}',
|
||||
].join('\n');
|
||||
document.head.appendChild(st);
|
||||
document.documentElement.style.setProperty('--primary', p);
|
||||
document.documentElement.style.setProperty('--secondary', s);
|
||||
}
|
||||
fetch('/branding').then(r=>r.json()).then(b=>{
|
||||
if(b.site_name){document.title=b.site_name;const bn=document.getElementById('brand-name');if(bn)bn.textContent=b.site_name;}
|
||||
if(b.site_subtitle){const bs=document.getElementById('brand-sub');if(bs)bs.textContent=b.site_subtitle;}
|
||||
applyBrandingColors(b.primary_color,b.secondary_color,b.bg_color,b.bg_image);
|
||||
if(b.logo_url){
|
||||
const logo=document.getElementById('brand-logo');
|
||||
if(logo){logo.src=b.logo_url;logo.style.height=(b.logo_size||'40')+'px';logo.classList.remove('hidden');}
|
||||
}
|
||||
}).catch(()=>{});
|
||||
|
||||
// ── Counter animation ─────────────────────────────────────────────────────────
|
||||
function animateCount(el, target, duration=900) {
|
||||
@@ -480,37 +372,6 @@ const observer = new IntersectionObserver(entries => {
|
||||
}, { threshold: 0.12 });
|
||||
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
async function initAuth() {
|
||||
const nav = document.getElementById('nav-user');
|
||||
if (!userToken) {
|
||||
nav.innerHTML = `<a href="/login" class="bg-primary text-surface font-bold px-3 py-2 rounded-xl text-xs hover:shadow-[0_0_15px_rgba(78,222,163,0.4)] transition-all flex items-center gap-1.5 whitespace-nowrap" style="animation:scalePop .4s cubic-bezier(.22,1,.36,1) .8s both"><span class="msym text-base">login</span><span>دخول</span></a>`;
|
||||
document.getElementById('upload-card').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/auth/me', {headers:{'Authorization':'Bearer '+userToken}});
|
||||
if (!r.ok) { localStorage.removeItem('rag_token'); initAuth(); return; }
|
||||
currentUser = await r.json();
|
||||
const perms = currentUser.permissions || [];
|
||||
if (!perms.includes('upload')) document.getElementById('upload-card').classList.add('hidden');
|
||||
nav.innerHTML = `
|
||||
<div class="flex items-center gap-2" style="animation:fadeIn .4s both .6s">
|
||||
<div class="text-right hidden sm:block">
|
||||
<p class="text-xs font-bold">${currentUser.name}</p>
|
||||
<p class="text-[10px] text-primary">${currentUser.role}</p>
|
||||
</div>
|
||||
${perms.includes('admin') ? '<a href="/admin" class="glass glass-hover p-2 rounded-xl transition-colors"><span class="msym text-base text-on-surface-variant">admin_panel_settings</span></a>' : ''}
|
||||
<button onclick="logout()" class="glass glass-hover p-2 rounded-xl transition-colors"><span class="msym text-base text-on-surface-variant">logout</span></button>
|
||||
</div>`;
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if(userToken) fetch('/auth/logout',{method:'POST',headers:{'Authorization':'Bearer '+userToken}});
|
||||
localStorage.removeItem('rag_token');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// ── Stats ─────────────────────────────────────────────────────────────────────
|
||||
async function loadStats() {
|
||||
@@ -683,7 +544,6 @@ spinStyle.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
|
||||
document.head.appendChild(spinStyle);
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
initAuth();
|
||||
loadStats();
|
||||
loadBooks();
|
||||
setInterval(loadStats, 30000);
|
||||
@@ -697,5 +557,7 @@ setInterval(loadStats, 30000);
|
||||
<span style="margin:0 8px">© 2026 أفاق زيد</span>
|
||||
</footer>
|
||||
|
||||
<script src="/components.js"></script>
|
||||
<div id="universal-footer"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-119
@@ -338,36 +338,7 @@ main {
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky top-0 z-20 glass border-b border-primary/20">
|
||||
<div class="max-w-6xl mx-auto px-4 lg:px-8 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 bg-primary/15 rounded-xl flex items-center justify-center glow-sm">
|
||||
<span class="msym text-primary text-lg">auto_awesome</span>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<h1 class="text-base font-bold text-primary leading-tight" id="brand-name">AfakZaid RAG</h1>
|
||||
<p class="text-[10px] text-on-surface-variant/60 uppercase tracking-widest">Scholarly Intelligence</p>
|
||||
</div>
|
||||
</a>
|
||||
<div class="h-5 w-px bg-outline-variant/40 hidden sm:block"></div>
|
||||
<span class="text-sm font-semibold text-on-surface flex items-center gap-2">
|
||||
<span class="msym text-primary text-base">library_books</span> المكتبة
|
||||
</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-2">
|
||||
<a href="/" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="msym text-base">search</span>
|
||||
<span class="hidden sm:inline">البحث الذكي</span>
|
||||
</a>
|
||||
<button onclick="showFavorites()" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-red-400 transition-colors">
|
||||
<span class="msym text-base">favorite</span>
|
||||
<span class="hidden sm:inline">المفضلة</span>
|
||||
</button>
|
||||
<div id="nav-auth" class="flex items-center gap-2"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div id="universal-header"></div>
|
||||
|
||||
<main class="max-w-6xl" mx-auto px-4 lg:px-8 py-8">
|
||||
|
||||
@@ -649,98 +620,11 @@ async function toggleFav(bookId, btn, e) {
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── Branding ───────────────────────────────────────────────────────────────
|
||||
|
||||
// -- Branding color override --------------------------------------------------
|
||||
function applyBrandingColors(p, s, bg, bgImg) {
|
||||
p = p || '#4edea3';
|
||||
s = s || '#95d3ba';
|
||||
bg = bg || '#0c1324';
|
||||
const hr = (hex) => {
|
||||
hex = hex.replace('#','');
|
||||
if(hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||||
return parseInt(hex.slice(0,2),16)+','+parseInt(hex.slice(2,4),16)+','+parseInt(hex.slice(4,6),16);
|
||||
};
|
||||
const pr = hr(p), sr = hr(s), bgr = hr(bg);
|
||||
// Background
|
||||
if(bgImg) { document.body.style.backgroundImage='url('+bgImg+')'; document.body.style.backgroundSize='cover'; document.body.style.backgroundPosition='center'; }
|
||||
else { document.body.style.backgroundImage=''; document.body.style.background='radial-gradient(circle at 50% -20%,'+bg+' 0%,'+bg+' 100%)'; }
|
||||
// Remove old style
|
||||
const old = document.getElementById('_bco'); if(old) old.remove();
|
||||
const st = document.createElement('style'); st.id='_bco';
|
||||
st.textContent = [
|
||||
// Primary color — text, bg, border
|
||||
'.text-primary{color:'+p+'!important}',
|
||||
'.bg-primary{background-color:'+p+'!important}',
|
||||
'.border-primary{border-color:'+p+'!important}',
|
||||
'[class*="text-primary"]{color:'+p+'!important}',
|
||||
'[class*="bg-primary\/"]{}',
|
||||
// Secondary
|
||||
'.text-secondary{color:'+s+'!important}',
|
||||
'.bg-secondary{background-color:'+s+'!important}',
|
||||
'.border-secondary{border-color:'+s+'!important}',
|
||||
'.text-tertiary{color:'+s+'!important}',
|
||||
// Hover/focus states
|
||||
'.hover\:text-primary:hover{color:'+p+'!important}',
|
||||
'.hover\:bg-primary\/5:hover{background-color:rgba('+pr+',0.05)!important}',
|
||||
'.focus\:border-primary:focus{border-color:'+p+'!important}',
|
||||
// Active nav
|
||||
'.active.bg-primary\/10{background-color:rgba('+pr+',0.1)!important}',
|
||||
'.active.text-primary{color:'+p+'!important}',
|
||||
'button.active{color:'+p+'!important}',
|
||||
// Glass
|
||||
'.glass{border-color:rgba('+pr+',0.12)!important}',
|
||||
'.glass-hover:hover{background:rgba('+pr+',0.06)!important;border-color:rgba('+pr+',0.25)!important}',
|
||||
// Glows
|
||||
'.glow{box-shadow:0 0 20px rgba('+pr+',0.15)!important}',
|
||||
'.glow-sm{box-shadow:0 0 10px rgba('+pr+',0.1)!important}',
|
||||
'.glass.glow{box-shadow:inset 0 1px 0 rgba(255,255,255,0.06),0 0 20px rgba('+pr+',0.18),0 4px 24px rgba(0,0,0,0.15)!important}',
|
||||
'.glass.glow-sm{box-shadow:inset 0 1px 0 rgba(255,255,255,0.06),0 0 10px rgba('+pr+',0.12)!important}',
|
||||
// Spotlight card gradient
|
||||
'.spotlight-card::before{background:radial-gradient(400px circle at var(--mx,50%) var(--my,50%),rgba('+pr+',0.06),transparent 60%)!important}',
|
||||
// Alpha variants
|
||||
'.bg-primary\/15,.bg-primary\/10,.bg-primary\/5{background-color:rgba('+pr+',0.1)!important}',
|
||||
'.border-primary\/20{border-color:rgba('+pr+',0.2)!important}',
|
||||
'.border-primary\/30{border-color:rgba('+pr+',0.3)!important}',
|
||||
// Upload zone
|
||||
'.upload-zone{border-color:rgba('+pr+',0.2)!important}',
|
||||
'.upload-zone.dragover{border-color:rgba('+pr+',0.6)!important}',
|
||||
// Scrollbar
|
||||
'.scrollbar::-webkit-scrollbar-thumb{background:rgba('+pr+',0.2)!important}',
|
||||
// Input focus
|
||||
'.inp:focus,.inp:focus-within{border-color:'+p+'!important;box-shadow:0 0 0 2px rgba('+pr+',0.1)!important}',
|
||||
// Filter chips active
|
||||
'.filter-chip.active{background:rgba('+pr+',0.12)!important;border-color:rgba('+pr+',0.4)!important;color:'+p+'!important}',
|
||||
// Book card hover
|
||||
'.book-card:hover{box-shadow:0 8px 24px rgba('+pr+',0.1)!important;border-color:rgba('+pr+',0.28)!important}',
|
||||
// Nav active
|
||||
'.nav-btn.active{background-color:rgba('+pr+',0.1)!important;color:'+p+'!important}',
|
||||
// Sidebar border
|
||||
'aside{border-color:rgba('+pr+',0.2)!important}',
|
||||
// CTA button shimmer
|
||||
'.cta-primary::before{background:linear-gradient(90deg,transparent,rgba('+pr+',0.1),transparent)!important}',
|
||||
// Shadow glow on hover
|
||||
'[class*="hover:shadow"]:hover{--tw-shadow-color:rgba('+pr+',0.4)!important}',
|
||||
].join('\n');
|
||||
document.head.appendChild(st);
|
||||
document.documentElement.style.setProperty('--primary', p);
|
||||
document.documentElement.style.setProperty('--secondary', s);
|
||||
}
|
||||
fetch('/branding').then(r=>r.json()).then(b=>{
|
||||
if(b.site_name){const bn=document.getElementById('brand-name');if(bn)bn.textContent=b.site_name;}
|
||||
applyBrandingColors(b.primary_color,b.secondary_color,b.bg_color,b.bg_image);
|
||||
}).catch(()=>{});
|
||||
|
||||
|
||||
// ── Auth Nav ───────────────────────────────────────────────────────────────
|
||||
async function initAuth() {
|
||||
const nav = document.getElementById('nav-auth');
|
||||
if (!userToken) {
|
||||
nav.innerHTML = `<a href="/login" class="glass glass-hover flex items-center gap-2 px-3 py-2 rounded-xl text-xs text-on-surface-variant hover:text-primary transition-colors"><span class="msym text-base">login</span><span class="hidden sm:inline">دخول</span></a>`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/auth/me', {headers:{'Authorization':'Bearer '+userToken}});
|
||||
if (!r.ok) { localStorage.removeItem('rag_token'); initAuth(); return; }
|
||||
return; }
|
||||
currentUser = await r.json();
|
||||
checkActiveImport();
|
||||
nav.innerHTML = `<div class="relative" id="user-menu-wrap">
|
||||
@@ -1682,5 +1566,7 @@ document.querySelectorAll('.spotlight-card').forEach(card => {
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div id="universal-footer"></div>
|
||||
<script src="/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user