/** * Q-Simplified — Shared Utilities * Include this in every page: */ // ─── CONFIG ──────────────────────────────────────────────────────────────── const QS = { API_BASE: '', // Same origin // ← Update after Railway deploy VERSION: '1.0.0', }; // ─── AUTH MODULE ─────────────────────────────────────────────────────────── const Auth = { getToken() { const token = localStorage.getItem('qs_token'); const exp = +localStorage.getItem('qs_token_exp'); if (!token || Date.now() > exp) return null; return token; }, getUser() { try { return JSON.parse(localStorage.getItem('qs_user') || 'null'); } catch { return null; } }, isLoggedIn() { return !!this.getToken(); }, save(data) { localStorage.setItem('qs_token', data.access_token); localStorage.setItem('qs_user', JSON.stringify(data.user)); localStorage.setItem('qs_token_exp', Date.now() + data.expires_in * 1000); }, logout() { ['qs_token', 'qs_user', 'qs_token_exp'].forEach(k => localStorage.removeItem(k)); window.location.href = '/login.html'; }, requireAuth(redirectAfter = window.location.href) { if (!this.isLoggedIn()) { window.location.href = `/login.html?redirect=${encodeURIComponent(redirectAfter)}`; return false; } return true; }, authHeaders() { const token = this.getToken(); return token ? { 'Authorization': `Bearer ${token}` } : {}; }, }; // ─── API MODULE ──────────────────────────────────────────────────────────── const API = { async get(path) { const res = await fetch(`${QS.API_BASE}${path}`, { headers: { ...Auth.authHeaders(), 'Content-Type': 'application/json' } }); if (res.status === 401) { Auth.logout(); return null; } if (!res.ok) throw new Error(`API Error ${res.status}`); return res.json(); }, async post(path, body) { const res = await fetch(`${QS.API_BASE}${path}`, { method: 'POST', headers: { ...Auth.authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Request failed'); return data; }, async put(path, body) { const res = await fetch(`${QS.API_BASE}${path}`, { method: 'PUT', headers: { ...Auth.authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Request failed'); return data; }, async delete(path) { const res = await fetch(`${QS.API_BASE}${path}`, { method: 'DELETE', headers: { ...Auth.authHeaders() }, }); if (res.status === 204) return null; const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Request failed'); return data; }, async postForm(path, formData) { const res = await fetch(`${QS.API_BASE}${path}`, { method: 'POST', headers: { ...Auth.authHeaders() }, body: formData, }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Upload failed'); return data; }, // Convenience market: { getLatest: () => API.get('/api/market-data/latest'), getHistory: (symbol, period='1mo', interval) => API.get(`/api/market-data/history/${encodeURIComponent(symbol)}?period=${period}${interval ? '&interval=' + interval : ''}`), }, news: { getInsights: () => API.get('/api/news/live-insights'), getLatest: (limit=20, category=null) => API.get(`/api/news/latest?limit=${limit}${category?'&category='+category:''}`), }, blogs: { getAll: (opts={}) => API.get(`/api/blogs?limit=${opts.limit||20}&offset=${opts.offset||0}${opts.category?'&category='+opts.category:''}`), getFeatured: () => API.get('/api/blogs/featured'), getRecent: (n=3) => API.get(`/api/blogs/recent?limit=${n}`), getOne: (slug) => API.get(`/api/blogs/${slug}`), create: (body) => API.post('/api/blogs', body), update: (slug, body) => API.put(`/api/blogs/${slug}`, body), remove: (slug) => API.delete(`/api/blogs/${slug}`), getMine: (published) => API.get(`/api/blogs/mine${published !== undefined ? '?published=' + published : ''}`), }, upload: { image: (file) => { const fd = new FormData(); fd.append('file', file); return API.postForm('/api/upload/image', fd); }, }, }; // ─── NAV AUTO-UPDATE ─────────────────────────────────────────────────────── function initNav() { const user = Auth.getUser(); const btn = document.getElementById('nav-auth-btn'); if (!btn) return; if (user && Auth.isLoggedIn()) { btn.textContent = user.full_name?.split(' ')[0] || 'Account'; btn.href = '/profile.html'; btn.style.background = 'rgba(255,255,255,0.1)'; } } // ─── TOAST NOTIFICATIONS ────────────────────────────────────────────────── function showToast(msg, type='info', duration=3000) { const colors = { info:'#003465', success:'#1a7a4a', error:'#c0392b' }; const toast = document.createElement('div'); toast.style.cssText = ` position:fixed;bottom:1.5rem;right:1.5rem;z-index:9999; background:${colors[type]||colors.info};color:#fff; padding:0.875rem 1.25rem;border-radius:9999px; font-family:'Work Sans',sans-serif;font-size:0.875rem;font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.2); animation:slideIn 0.3s ease; max-width:340px; `; toast.textContent = msg; document.body.appendChild(toast); if (!document.getElementById('toast-style')) { const s = document.createElement('style'); s.id = 'toast-style'; s.textContent = '@keyframes slideIn{from{transform:translateY(1rem);opacity:0}to{transform:translateY(0);opacity:1}}'; document.head.appendChild(s); } setTimeout(() => { toast.style.opacity='0'; toast.style.transition='opacity 0.3s'; setTimeout(()=>toast.remove(),300); }, duration); } // ─── FORMAT HELPERS ─────────────────────────────────────────────────────── const Format = { currency: (n) => '₹' + n.toLocaleString('en-IN'), number: (n) => n.toLocaleString('en-IN'), pct: (n) => (n > 0 ? '+' : '') + n.toFixed(2) + '%', timeAgo(dateStr) { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); const hrs = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (mins < 60) return mins <= 1 ? 'Just now' : `${mins} mins ago`; if (hrs < 24) return `${hrs} hour${hrs>1?'s':''} ago`; if (days < 7) return `${days} day${days>1?'s':''} ago`; return new Date(dateStr).toLocaleDateString('en-IN', { day:'numeric', month:'short', year:'numeric' }); }, }; // ─── INIT ON PAGE LOAD ───────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', initNav); // Export for use in page scripts window.QS = QS; window.Auth = Auth; window.API = API; window.Format = Format; window.showToast = showToast;