feat: /live page — full ship log + oops on quikcue.com
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
<a href="/" class="logo">QuikCue</a>
|
||||
<div class="nav-links">
|
||||
<a href="/#work">Work</a>
|
||||
<a href="/#live">Live Feed</a>
|
||||
<a href="/live">Live Feed</a>
|
||||
<a href="/#contact" class="nav-cta">Talk to us</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -840,3 +840,214 @@ nav {
|
||||
@media (max-width: 380px) {
|
||||
.nav-links a:not(.nav-cta) { display: none; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
/LIVE PAGE — Full Shipping Log
|
||||
═══════════════════════════════════════════ */
|
||||
.live-page {
|
||||
padding-top: 96px;
|
||||
padding-bottom: 5rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.live-page-header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.live-page-eyebrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.live-page-title {
|
||||
font-family: var(--sans);
|
||||
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: var(--text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.live-page-sub {
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.75;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.log-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin: 2.5rem 0 2rem;
|
||||
}
|
||||
.filter-btn {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 99px;
|
||||
border: 1px solid var(--border-light);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.filter-btn:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: var(--accent);
|
||||
color: #050505;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Day groups */
|
||||
.log-day {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.log-day-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.log-day-date {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.log-day-count {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Log entries */
|
||||
.log-entry {
|
||||
padding: 0.85rem 1.25rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
.log-entry:hover {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.log-entry-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.log-entry-id {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
min-width: 2.2rem;
|
||||
}
|
||||
.log-entry-title {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-secondary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.log-metric {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Oops section */
|
||||
.oops-section {
|
||||
margin-top: 4rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.oops-title {
|
||||
font-family: var(--mono);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.oops-sub {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.oops-entry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #ef4444;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.oops-id {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Live page CTA */
|
||||
.live-page-cta {
|
||||
text-align: center;
|
||||
margin-top: 5rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.live-page-cta p {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.log-loading {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-dim);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Active nav link */
|
||||
.nav-links a.active {
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
/* ── Responsive: /live page ── */
|
||||
@media (max-width: 640px) {
|
||||
.live-page { padding-top: 80px; }
|
||||
.log-entry-head { gap: 0.4rem; }
|
||||
.log-entry-id { display: none; }
|
||||
.log-filters { flex-wrap: wrap; }
|
||||
.oops-entry { flex-direction: column; gap: 0.25rem; }
|
||||
}
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@
|
||||
<a href="/" class="logo">QuikCue</a>
|
||||
<div class="nav-links">
|
||||
<a href="#work">Work</a>
|
||||
<a href="#live">Live Feed</a>
|
||||
<a href="/live">Live Feed</a>
|
||||
<a href="#contact" class="nav-cta">Talk to us</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@
|
||||
</p>
|
||||
<div class="hero-ctas">
|
||||
<a href="#contact" class="btn btn-primary">Start a conversation →</a>
|
||||
<a href="#live" class="btn btn-ghost">Watch us build ◉</a>
|
||||
<a href="/live" class="btn btn-ghost">Watch us build ◉</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-glow"></div>
|
||||
@@ -204,6 +204,7 @@
|
||||
<div class="live-feed-header">
|
||||
<div class="live-pulse"></div>
|
||||
<span>Live Engineering Feed</span>
|
||||
<a href="/live" class="live-feed-link">View full log →</a>
|
||||
</div>
|
||||
<div class="live-feed-entries" id="live-entries">
|
||||
<div class="live-entry live-entry-loading">
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>QuikCue — Live Shipping Log</title>
|
||||
<meta name="description" content="Every deployment, every fix, every system — logged and live. Real-time engineering feed from QuikCue.">
|
||||
<meta property="og:title" content="QuikCue — Live Shipping Log">
|
||||
<meta property="og:description" content="Real-time engineering feed. Every deployment logged. Full transparency.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://quikcue.com/live">
|
||||
<link rel="canonical" href="https://quikcue.com/live">
|
||||
<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=JetBrains+Mono:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="logo">QuikCue</a>
|
||||
<div class="nav-links">
|
||||
<a href="/#work">Work</a>
|
||||
<a href="/live" class="active">Live Feed</a>
|
||||
<a href="/#contact" class="nav-cta">Talk to us</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="live-page">
|
||||
<div class="container">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="live-page-header">
|
||||
<div class="live-page-eyebrow">
|
||||
<div class="live-pulse"></div>
|
||||
<span>Live Engineering Feed</span>
|
||||
</div>
|
||||
<h1 class="live-page-title">Full Shipping Log</h1>
|
||||
<p class="live-page-sub">
|
||||
Every system we've shipped. Every fix we've made. Unfakeable, unedited, pulled
|
||||
straight from our PostgreSQL database in real time.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="live-stats" id="stats-bar">
|
||||
<div class="live-stat">
|
||||
<div class="live-stat-num" id="stat-total">—</div>
|
||||
<div class="live-stat-label">Total ships</div>
|
||||
</div>
|
||||
<div class="live-stat">
|
||||
<div class="live-stat-num live-stat-green" id="stat-shipped">—</div>
|
||||
<div class="live-stat-label">Shipped</div>
|
||||
</div>
|
||||
<div class="live-stat">
|
||||
<div class="live-stat-num live-stat-amber" id="stat-shipping">—</div>
|
||||
<div class="live-stat-label">In flight</div>
|
||||
</div>
|
||||
<div class="live-stat">
|
||||
<div class="live-stat-num live-stat-red" id="stat-oops">—</div>
|
||||
<div class="live-stat-label">Oops</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="log-filters">
|
||||
<button class="filter-btn active" data-filter="all">All</button>
|
||||
<button class="filter-btn" data-filter="shipped">Shipped</button>
|
||||
<button class="filter-btn" data-filter="shipping">In flight</button>
|
||||
</div>
|
||||
|
||||
<!-- Full Ship Log -->
|
||||
<div class="log-list" id="log-list">
|
||||
<div class="log-loading">Loading from database…</div>
|
||||
</div>
|
||||
|
||||
<!-- Oops Log -->
|
||||
<section class="oops-section" id="oops-section">
|
||||
<h2 class="oops-title">Oops Log</h2>
|
||||
<p class="oops-sub">If it's not here, we haven't broken it yet.</p>
|
||||
<div class="oops-list" id="oops-list">
|
||||
<div class="log-loading">Loading…</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="live-page-cta">
|
||||
<p>This is what autonomous operations looks like.</p>
|
||||
<a href="/#contact" class="btn btn-primary">Start a conversation →</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand">
|
||||
<span class="footer-logo">QuikCue</span>
|
||||
<span class="footer-tagline">We build the machine. You run the mission.</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="/live">Live Feed</a>
|
||||
<a href="https://git.quikcue.com" target="_blank">Git</a>
|
||||
<a href="mailto:omair@quikcue.com">Contact</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 2026 QuikCue Ltd. London, UK.</span>
|
||||
<span class="footer-built">Built with obsessive velocity.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const API = 'https://calvana.quikcue.com/api';
|
||||
|
||||
// ── Load everything ──
|
||||
async function init() {
|
||||
const [ships, oops, stats] = await Promise.all([
|
||||
fetch(`${API}/ships`).then(r => r.json()).catch(() => []),
|
||||
fetch(`${API}/oops`).then(r => r.json()).catch(() => []),
|
||||
fetch(`${API}/stats`).then(r => r.json()).catch(() => ({ total: 0, shipped: 0, oops: 0 }))
|
||||
]);
|
||||
|
||||
renderStats(stats, ships);
|
||||
renderShips(ships);
|
||||
renderOops(oops);
|
||||
initFilters(ships);
|
||||
}
|
||||
|
||||
// ── Stats ──
|
||||
function renderStats(stats, ships) {
|
||||
const inFlight = ships.filter(s => s.status === 'shipping').length;
|
||||
animateNum('stat-total', stats.total);
|
||||
animateNum('stat-shipped', stats.shipped);
|
||||
animateNum('stat-shipping', inFlight);
|
||||
animateNum('stat-oops', stats.oops);
|
||||
}
|
||||
|
||||
function animateNum(id, target) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !target) { if (el) el.textContent = target || '0'; return; }
|
||||
const start = performance.now();
|
||||
const dur = 800;
|
||||
(function step(now) {
|
||||
const p = Math.min((now - start) / dur, 1);
|
||||
const eased = 1 - Math.pow(1 - p, 3);
|
||||
el.textContent = Math.floor(target * eased);
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
})(start);
|
||||
}
|
||||
|
||||
// ── Ships ──
|
||||
function renderShips(ships) {
|
||||
const container = document.getElementById('log-list');
|
||||
|
||||
// Group by date
|
||||
const groups = {};
|
||||
ships.forEach(s => {
|
||||
const date = (s.created_at || '').split('T')[0] || 'Unknown';
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(s);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
for (const [date, entries] of Object.entries(groups)) {
|
||||
const formatted = formatDate(date);
|
||||
html += `<div class="log-day">
|
||||
<div class="log-day-header">
|
||||
<span class="log-day-date">${formatted}</span>
|
||||
<span class="log-day-count">${entries.length} ship${entries.length !== 1 ? 's' : ''}</span>
|
||||
</div>`;
|
||||
|
||||
entries.forEach(s => {
|
||||
const badgeClass = s.status === 'shipped' ? 'badge-shipped' :
|
||||
s.status === 'shipping' ? 'badge-shipping' : 'badge-planned';
|
||||
const label = s.status.charAt(0).toUpperCase() + s.status.slice(1);
|
||||
const metric = s.metric && s.metric !== '-' && s.metric !== '—'
|
||||
? `<div class="log-metric">${escapeHtml(s.metric)}</div>` : '';
|
||||
|
||||
html += `<div class="log-entry" data-status="${s.status}">
|
||||
<div class="log-entry-head">
|
||||
<span class="log-entry-id">#${s.id}</span>
|
||||
<span class="live-entry-badge ${badgeClass}">${label}</span>
|
||||
<span class="log-entry-title">${escapeHtml(s.title)}</span>
|
||||
</div>
|
||||
${metric}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Oops ──
|
||||
function renderOops(oops) {
|
||||
const container = document.getElementById('oops-list');
|
||||
if (!oops.length) {
|
||||
container.innerHTML = '<div class="oops-entry">Nothing broken yet. Give it time.</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = oops.map(o => {
|
||||
const time = o.fix_time && o.fix_time !== '-' ? ` — fixed in ${escapeHtml(o.fix_time)}` : '';
|
||||
return `<div class="oops-entry">
|
||||
<span class="oops-id">#${o.id}</span>
|
||||
<span>${escapeHtml(o.description)}${time}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Filters ──
|
||||
function initFilters(ships) {
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const filter = btn.dataset.filter;
|
||||
document.querySelectorAll('.log-entry').forEach(entry => {
|
||||
if (filter === 'all' || entry.dataset.status === filter) {
|
||||
entry.style.display = '';
|
||||
} else {
|
||||
entry.style.display = 'none';
|
||||
}
|
||||
});
|
||||
// Hide empty day headers
|
||||
document.querySelectorAll('.log-day').forEach(day => {
|
||||
const visible = day.querySelectorAll('.log-entry:not([style*="display: none"])');
|
||||
day.style.display = visible.length ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
function formatDate(str) {
|
||||
try {
|
||||
const d = new Date(str + 'T00:00:00');
|
||||
return d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
||||
} catch { return str; }
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Live pulse
|
||||
setInterval(() => {
|
||||
const p = document.querySelector('.live-pulse');
|
||||
if (p) { p.classList.remove('ping'); void p.offsetWidth; p.classList.add('ping'); }
|
||||
}, 3000);
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user