q-simplified / static /js /app.js
SRVCP's picture
Deploy market history and blog uploader updates
7a0d219
/**
* Q-Simplified β€” Shared Utilities
* Include this in every page: <script src="/js/app.js"></script>
*/
// ─── 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;