feat: complete AfakZaid RAG system v1.7.4
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="dark" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>AfakZaid RAG — Sign In</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script>tailwind.config={darkMode:"class",theme:{extend:{colors:{"primary":"#4edea3","surface":"#0c1324","surface-low":"#151b2d","surface-container":"#191f31","surface-high":"#23293c","on-surface":"#dce1fb","on-surface-variant":"#bbcabf","outline-variant":"#3c4a42","secondary":"#95d3ba","error":"#f87171"},fontFamily:{sans:["Manrope","sans-serif"]}}}}</script>
|
||||
<style>
|
||||
*{box-sizing:border-box}
|
||||
body{background:radial-gradient(circle at 50% -20%,#151b2d 0%,#0c1324 100%);font-family:'Manrope',sans-serif;color:#dce1fb;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
|
||||
.glass{background:rgba(21,27,45,0.6);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(78,222,163,0.15)}
|
||||
.glow{box-shadow:0 0 30px rgba(78,222,163,0.15)}
|
||||
.msym{font-family:'Material Symbols Outlined';font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24;vertical-align:middle;line-height:1}
|
||||
:root { --ease-out: cubic-bezier(0.23,1,0.32,1); }
|
||||
|
||||
.inp{width:100%;background:#23293c;border:1px solid #3c4a42;border-radius:12px;padding:12px 16px;color:#dce1fb;font-size:14px;outline:none;transition:border-color 180ms ease, box-shadow 180ms ease;font-family:'Manrope',sans-serif}
|
||||
.inp:focus{border-color:#4edea3;box-shadow:0 0 0 2px rgba(78,222,163,0.1)}
|
||||
.inp.inp-error{border-color:#f87171;box-shadow:0 0 0 2px rgba(248,113,113,0.1)}
|
||||
.inp-icon{position:relative}
|
||||
.inp-icon .msym{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#bbcabf;font-size:18px;pointer-events:none}
|
||||
.inp-icon input{padding-left:40px}
|
||||
|
||||
/* Primary button — specific transitions, no transition:all */
|
||||
.btn-primary{width:100%;background:#4edea3;color:#003824;font-weight:700;border:none;border-radius:12px;padding:14px;cursor:pointer;font-size:15px;font-family:'Manrope',sans-serif;position:relative;overflow:hidden;transition:box-shadow 200ms ease, transform 150ms var(--ease-out)}
|
||||
.btn-primary:disabled{opacity:0.5;cursor:not-allowed}
|
||||
.btn-primary:active:not(:disabled){transform:scale(0.97);transition-duration:100ms}
|
||||
@media (hover:hover) and (pointer:fine){.btn-primary:hover:not(:disabled){box-shadow:0 0 24px rgba(78,222,163,0.4)}}
|
||||
|
||||
/* Shimmer on btn hover */
|
||||
.btn-primary::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,0.12) 50%,transparent 100%);transform:translateX(-100%);transition:transform 380ms cubic-bezier(0.77,0,0.175,1);pointer-events:none}
|
||||
@media (hover:hover) and (pointer:fine){.btn-primary:hover::after{transform:translateX(100%)}}
|
||||
|
||||
/* Tabs — specific transitions */
|
||||
.tab{padding:10px 24px;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:transparent;color:#bbcabf;font-family:'Manrope',sans-serif;transition:background 150ms var(--ease-out),color 150ms var(--ease-out)}
|
||||
.tab.active{background:rgba(78,222,163,0.1);color:#4edea3}
|
||||
.tab:active{transform:scale(0.97)}
|
||||
|
||||
/* Field validation */
|
||||
.field-error{font-size:12px;color:#f87171;margin-top:4px;display:none}
|
||||
.field-error.visible{display:block}
|
||||
|
||||
/* Country dropdown */
|
||||
#country-dropdown{position:absolute;top:calc(100% + 4px);left:0;right:0;background:#191f31;border:1px solid rgba(78,222,163,0.2);border-radius:12px;z-index:50;max-height:200px;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.4)}
|
||||
#country-dropdown::-webkit-scrollbar{width:4px}
|
||||
#country-dropdown::-webkit-scrollbar-thumb{background:rgba(78,222,163,0.2);border-radius:4px}
|
||||
.country-item{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;font-size:13px;transition:background 120ms ease}
|
||||
@media (hover:hover) and (pointer:fine){.country-item:hover{background:rgba(78,222,163,0.08)}}
|
||||
|
||||
/* Form entry */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||
.auth-card{animation:fadeUp 400ms var(--ease-out) both}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms !important;transition-duration:.01ms !important}}
|
||||
|
||||
|
||||
/* ── Mobile-visible improvements ─────────────────────────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
button, a, .filter-chip, .book-card, .stat-card, .nav-btn {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Tap feedback — visible on ALL devices including mobile */
|
||||
.book-card:active,
|
||||
.stat-card:active,
|
||||
.filter-chip:active {
|
||||
transform: scale(0.97) !important;
|
||||
transition: transform 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Buttons get a visible press state on tap */
|
||||
button:active:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
transform: scale(0.97);
|
||||
transition: transform 100ms ease, filter 100ms ease !important;
|
||||
}
|
||||
|
||||
/* Restore .glow — ensure it stacks correctly */
|
||||
.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
0 0 20px rgba(78,222,163,0.15) !important;
|
||||
}
|
||||
.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.04),
|
||||
0 0 10px rgba(78,222,163,0.1) !important;
|
||||
}
|
||||
|
||||
/* Mobile: book cards get a more visible pressed state */
|
||||
@media (max-width: 768px) {
|
||||
.book-card {
|
||||
transition: transform 150ms ease, opacity 150ms ease !important;
|
||||
}
|
||||
.book-card:active {
|
||||
transform: scale(0.97) !important;
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
/* Ensure skeleton is visible on mobile */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #1a2035 25%, #2a3348 50%, #1a2035 75%) !important;
|
||||
background-size: 800px 100% !important;
|
||||
animation: shimmer 1.2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
/* Better spacing on small screens */
|
||||
.filter-chip {
|
||||
padding: 8px 16px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
/* ── design-taste-frontend: Liquid Glass + Spotlight ─────────────────────── */
|
||||
|
||||
/* True liquid glass — inner refraction border */
|
||||
/* NOTE: does NOT define outer shadow — .glow still wins */
|
||||
.glass {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
/* Combined: liquid glass + green glow */
|
||||
.glass.glow {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 20px rgba(78,222,163,0.18),
|
||||
0 4px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
.glass.glow-sm {
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255,255,255,0.06),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.2),
|
||||
0 0 10px rgba(78,222,163,0.12);
|
||||
}
|
||||
/* .glow overrides naturally on top — no !important needed */
|
||||
|
||||
/* Spotlight border card — JS-driven cursor tracking */
|
||||
.spotlight-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spotlight-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
400px circle at var(--mx, 50%) var(--my, 50%),
|
||||
rgba(78,222,163,0.06),
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
z-index: 0;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.spotlight-card:hover::before { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Ensure content above spotlight overlay */
|
||||
.spotlight-card > * { position: relative; z-index: 1; }
|
||||
|
||||
/* Tactile button press — design-taste rule */
|
||||
button:not(:disabled):active,
|
||||
a[role=button]:active {
|
||||
transform: translateY(-1px) scale(0.98) !important;
|
||||
transition-duration: 80ms !important;
|
||||
}
|
||||
|
||||
/* Hardware acceleration on animated elements */
|
||||
.stat-card, .book-card, .query-card, .books-card, .upload-card-wrap {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Directional hover-aware CTA — shimmer from left on hover */
|
||||
.cta-primary {
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cta-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -100%; bottom: 0; width: 50%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 400ms cubic-bezier(0.77,0,0.175,1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.cta-primary:hover::before { left: 200%; }
|
||||
}
|
||||
|
||||
/* ── Mobile improvements (actually visible on mobile) ────────────────────── */
|
||||
|
||||
/* Larger touch targets — 44px minimum per Apple HIG */
|
||||
@media (max-width: 768px) {
|
||||
/* Bigger tap areas on buttons */
|
||||
button, a { min-height: 44px; }
|
||||
.filter-chip { padding: 8px 16px; font-size: 13px; }
|
||||
|
||||
/* Book cards: more breathing room on mobile */
|
||||
.book-card { padding: 14px !important; }
|
||||
|
||||
/* Stats cards: larger numbers on mobile */
|
||||
.stat-card .stat-num, #s-books, #s-vectors, #s-convos, #s-memory {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
/* Header: tighter on mobile */
|
||||
header { padding-top: 10px; padding-bottom: 10px; }
|
||||
|
||||
/* Search input: larger font so iOS doesn't zoom */
|
||||
#search-input, .inp, textarea, input[type="text"], input[type="email"], input[type="password"] {
|
||||
font-size: 16px !important; /* prevents iOS zoom on focus */
|
||||
}
|
||||
|
||||
/* Bottom action bar spacing */
|
||||
main { padding-bottom: env(safe-area-inset-bottom, 16px); }
|
||||
}
|
||||
|
||||
/* Reader: full height on mobile */
|
||||
@media (max-width: 768px) {
|
||||
#pdf-frame { position: absolute; inset: 0; }
|
||||
.reader-header { padding: 8px 12px; }
|
||||
}
|
||||
</style>
|
||||
<script src="/static/analytics.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="w-full max-w-md">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mx-auto mb-4 glow">
|
||||
<span class="msym text-primary text-3xl">auto_awesome</span>
|
||||
</div>
|
||||
<h1 id="brand-name" class="text-2xl font-bold text-primary">AfakZaid RAG</h1>
|
||||
<p id="brand-sub" class="text-on-surface-variant text-sm mt-1">Scholarly Intelligence</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="glass glow rounded-3xl p-8 auth-card">
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 bg-surface-container rounded-xl p-1 mb-8">
|
||||
<button class="tab active flex-1" id="tab-login" onclick="switchTab('login')">Sign In</button>
|
||||
<button class="tab flex-1" id="tab-register" onclick="switchTab('register')">Create Account</button>
|
||||
</div>
|
||||
|
||||
<!-- LOGIN -->
|
||||
<form id="form-login" onsubmit="doLogin(event)">
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="inp-icon">
|
||||
<span class="msym">mail</span>
|
||||
<input class="inp" type="email" id="login-email" placeholder="Email address" required/>
|
||||
</div>
|
||||
<div class="inp-icon">
|
||||
<span class="msym">lock</span>
|
||||
<input class="inp" type="password" id="login-pass" placeholder="Password" required/>
|
||||
</div>
|
||||
</div>
|
||||
<p id="login-err" class="text-error text-xs mb-4 hidden"></p>
|
||||
<button class="btn-primary" type="submit" id="login-btn">
|
||||
<span class="msym text-lg align-middle mr-1">login</span> Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- REGISTER -->
|
||||
<form id="form-register" class="hidden" onsubmit="doRegister(event)">
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="inp-icon">
|
||||
<span class="msym">person</span>
|
||||
<input class="inp" type="text" id="reg-name" placeholder="Full name" required/>
|
||||
</div>
|
||||
<div class="inp-icon">
|
||||
<span class="msym">mail</span>
|
||||
<input class="inp" type="email" id="reg-email" placeholder="Email address" required/>
|
||||
</div>
|
||||
<div class="inp-icon">
|
||||
<span class="msym">lock</span>
|
||||
<input class="inp" type="password" id="reg-pass" placeholder="Password (min 6 chars)" required minlength="6"/>
|
||||
</div>
|
||||
|
||||
<!-- Phone + Country -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<!-- Country picker -->
|
||||
<div class="relative" id="country-wrap">
|
||||
<div class="inp flex items-center gap-2 cursor-pointer select-none" onclick="toggleCountry()" id="country-display">
|
||||
<span class="msym text-on-surface-variant text-base">public</span>
|
||||
<span id="country-flag" class="text-lg hidden"></span>
|
||||
<span id="country-label" class="text-on-surface-variant text-sm truncate flex-1">Country</span>
|
||||
<span class="msym text-on-surface-variant text-base">expand_more</span>
|
||||
</div>
|
||||
<div id="country-dropdown" class="hidden">
|
||||
<div class="p-2">
|
||||
<input id="country-search" class="inp text-xs py-2" placeholder="Search country..." oninput="filterCountries(this.value)"/>
|
||||
</div>
|
||||
<div id="country-list"></div>
|
||||
</div>
|
||||
<input type="hidden" id="reg-country-code"/>
|
||||
<input type="hidden" id="reg-country-name"/>
|
||||
</div>
|
||||
<!-- Phone -->
|
||||
<div class="inp-icon">
|
||||
<span class="msym">phone</span>
|
||||
<input class="inp" type="tel" id="reg-phone" placeholder="Phone"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPS button -->
|
||||
<button type="button" onclick="detectLocation()" id="gps-btn"
|
||||
class="w-full flex items-center justify-center gap-2 glass border border-primary/20 text-on-surface-variant hover:text-primary hover:border-primary/50 rounded-xl py-2.5 text-sm transition-all">
|
||||
<span class="msym text-base">my_location</span>
|
||||
<span id="gps-text">Detect my location</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<p id="reg-err" class="text-error text-xs mb-4 hidden"></p>
|
||||
<button class="btn-primary" type="submit" id="reg-btn">
|
||||
<span class="msym text-lg align-middle mr-1">person_add</span> Create Account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="text-center text-on-surface-variant text-xs mt-6">
|
||||
<a href="/" class="hover:text-primary transition-colors">← Back to library</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Background -->
|
||||
<div class="fixed top-0 right-0 -z-10 w-96 h-96 bg-primary/5 rounded-full blur-[120px] pointer-events-none"></div>
|
||||
<div class="fixed bottom-0 left-0 -z-10 w-72 h-72 bg-secondary/5 rounded-full blur-[100px] pointer-events-none"></div>
|
||||
|
||||
<script>
|
||||
// ── Branding ───────────────────────────────────────────────────────────────
|
||||
fetch('/branding').then(r=>r.json()).then(b=>{
|
||||
if(b.site_name){document.title=b.site_name+' — Sign In';document.getElementById('brand-name').textContent=b.site_name;}
|
||||
if(b.site_subtitle) document.getElementById('brand-sub').textContent=b.site_subtitle;
|
||||
}).catch(()=>{});
|
||||
|
||||
// Redirect if already logged in
|
||||
const token = localStorage.getItem('rag_token');
|
||||
if(token) {
|
||||
fetch('/auth/me',{headers:{'Authorization':token}}).then(r=>{
|
||||
if(r.ok) window.location.href='/';
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
// ── Tabs ───────────────────────────────────────────────────────────────────
|
||||
function switchTab(tab) {
|
||||
document.getElementById('form-login').classList.toggle('hidden', tab!=='login');
|
||||
document.getElementById('form-register').classList.toggle('hidden', tab!=='register');
|
||||
document.getElementById('tab-login').classList.toggle('active', tab==='login');
|
||||
document.getElementById('tab-register').classList.toggle('active', tab==='register');
|
||||
}
|
||||
|
||||
// ── Auth ───────────────────────────────────────────────────────────────────
|
||||
async function doLogin(e) {
|
||||
e.preventDefault();
|
||||
const btn=document.getElementById('login-btn');
|
||||
if(btn.disabled) return;
|
||||
const err=document.getElementById('login-err');
|
||||
btn.disabled=true; btn.textContent='Signing in...'; err.classList.add('hidden');
|
||||
try {
|
||||
const r = await fetch('/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({email:document.getElementById('login-email').value,password:document.getElementById('login-pass').value})});
|
||||
const d = await r.json();
|
||||
if(!r.ok){err.textContent=d.detail||'Login failed';err.classList.remove('hidden');return;}
|
||||
localStorage.setItem('rag_token',d.token);
|
||||
localStorage.setItem('rag_user',JSON.stringify(d.user));
|
||||
window.location.href = d.user.permissions?.admin ? '/admin' : '/';
|
||||
} catch(ex){err.textContent='Connection error';err.classList.remove('hidden');}
|
||||
finally{btn.disabled=false;btn.innerHTML='<span class="msym text-lg align-middle mr-1">login</span> Sign In';}
|
||||
}
|
||||
|
||||
async function doRegister(e) {
|
||||
e.preventDefault();
|
||||
const btn=document.getElementById('reg-btn');
|
||||
if(btn.disabled) return;
|
||||
const err=document.getElementById('reg-err');
|
||||
btn.disabled=true;btn.textContent='Creating account...';err.classList.add('hidden');
|
||||
try {
|
||||
const r = await fetch('/auth/register',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({
|
||||
name:document.getElementById('reg-name').value,
|
||||
email:document.getElementById('reg-email').value,
|
||||
password:document.getElementById('reg-pass').value,
|
||||
phone:document.getElementById('reg-phone').value,
|
||||
country_code:document.getElementById('reg-country-code').value,
|
||||
country_name:document.getElementById('reg-country-name').value,
|
||||
})});
|
||||
const d = await r.json();
|
||||
if(!r.ok){err.textContent=d.detail||'Registration failed';err.classList.remove('hidden');return;}
|
||||
localStorage.setItem('rag_token',d.token);
|
||||
window.location.href='/';
|
||||
} catch(ex){err.textContent='Connection error';err.classList.remove('hidden');}
|
||||
finally{btn.disabled=false;btn.innerHTML='<span class="msym text-lg align-middle mr-1">person_add</span> Create Account';}
|
||||
}
|
||||
|
||||
// ── Country Picker ─────────────────────────────────────────────────────────
|
||||
let allCountries = [];
|
||||
let selectedCountry = null;
|
||||
|
||||
async function loadCountries() {
|
||||
try {
|
||||
const r = await fetch('https://restcountries.com/v3.1/all?fields=name,cca2,idd,flag');
|
||||
const data = await r.json();
|
||||
allCountries = data.sort((a,b)=>a.name.common.localeCompare(b.name.common));
|
||||
renderCountries(allCountries);
|
||||
} catch(e) {
|
||||
// fallback minimal list
|
||||
allCountries = [
|
||||
{name:{common:'Saudi Arabia'},cca2:'SA',flag:'🇸🇦'},
|
||||
{name:{common:'UAE'},cca2:'AE',flag:'🇦🇪'},
|
||||
{name:{common:'Kuwait'},cca2:'KW',flag:'🇰🇼'},
|
||||
{name:{common:'Qatar'},cca2:'QA',flag:'🇶🇦'},
|
||||
{name:{common:'Bahrain'},cca2:'BH',flag:'🇧🇭'},
|
||||
{name:{common:'Egypt'},cca2:'EG',flag:'🇪🇬'},
|
||||
{name:{common:'Jordan'},cca2:'JO',flag:'🇯🇴'},
|
||||
{name:{common:'United States'},cca2:'US',flag:'🇺🇸'},
|
||||
{name:{common:'United Kingdom'},cca2:'GB',flag:'🇬🇧'},
|
||||
];
|
||||
renderCountries(allCountries);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCountries(list) {
|
||||
document.getElementById('country-list').innerHTML = list.slice(0,100).map(c=>`
|
||||
<div class="country-item" onclick="selectCountry('${c.cca2}','${c.name.common.replace(/'/g,"\\'")}','${c.flag||''}')">
|
||||
<span class="text-lg">${c.flag||'🌍'}</span>
|
||||
<span class="text-sm">${c.name.common}</span>
|
||||
<span class="text-xs text-on-surface-variant ml-auto">${c.cca2}</span>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function filterCountries(q) {
|
||||
const filtered = allCountries.filter(c=>c.name.common.toLowerCase().includes(q.toLowerCase())||c.cca2.toLowerCase().includes(q.toLowerCase()));
|
||||
renderCountries(filtered);
|
||||
}
|
||||
|
||||
function selectCountry(code, name, flag) {
|
||||
document.getElementById('reg-country-code').value = code;
|
||||
document.getElementById('reg-country-name').value = name;
|
||||
document.getElementById('country-flag').textContent = flag;
|
||||
document.getElementById('country-flag').classList.remove('hidden');
|
||||
document.getElementById('country-label').textContent = name;
|
||||
document.getElementById('country-label').style.color = '#dce1fb';
|
||||
closeCountry();
|
||||
}
|
||||
|
||||
function toggleCountry() {
|
||||
const dd = document.getElementById('country-dropdown');
|
||||
dd.classList.toggle('hidden');
|
||||
if(!dd.classList.contains('hidden')) {
|
||||
document.getElementById('country-search').focus();
|
||||
loadCountries();
|
||||
}
|
||||
}
|
||||
|
||||
function closeCountry() {
|
||||
document.getElementById('country-dropdown').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.addEventListener('click', e=>{
|
||||
if(!document.getElementById('country-wrap').contains(e.target)) closeCountry();
|
||||
});
|
||||
|
||||
// ── GPS Detection ──────────────────────────────────────────────────────────
|
||||
async function detectLocation() {
|
||||
const btn = document.getElementById('gps-btn');
|
||||
const txt = document.getElementById('gps-text');
|
||||
txt.textContent = 'Detecting...';
|
||||
btn.disabled = true;
|
||||
if (!navigator.geolocation) { txt.textContent='GPS not supported'; btn.disabled=false; return; }
|
||||
navigator.geolocation.getCurrentPosition(async pos => {
|
||||
try {
|
||||
const {latitude: lat, longitude: lon} = pos.coords;
|
||||
const r = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`);
|
||||
const d = await r.json();
|
||||
const code = d.address?.country_code?.toUpperCase()||'';
|
||||
const name = d.address?.country||'';
|
||||
// Find flag
|
||||
await loadCountries();
|
||||
const country = allCountries.find(c=>c.cca2===code);
|
||||
const flag = country?.flag||'🌍';
|
||||
selectCountry(code, name, flag);
|
||||
txt.textContent = '✓ ' + name;
|
||||
} catch(e){ txt.textContent='Could not detect'; }
|
||||
btn.disabled=false;
|
||||
}, ()=>{ txt.textContent='Location denied'; btn.disabled=false; });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user