feat: live feed from calvana API + fallback data
This commit is contained in:
+103
-64
@@ -18,7 +18,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ NAV ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ NAV ═══════════════════════ -->
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-inner">
|
<div class="nav-inner">
|
||||||
<a href="/" class="logo">QuikCue</a>
|
<a href="/" class="logo">QuikCue</a>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ HERO ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ HERO ═══════════════════════ -->
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="hero-inner">
|
<div class="hero-inner">
|
||||||
<div class="hero-eyebrow">Autonomous Systems Studio</div>
|
<div class="hero-eyebrow">Autonomous Systems Studio</div>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<div class="hero-glow"></div>
|
<div class="hero-glow"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ PROBLEM ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ PROBLEM ═══════════════════════ -->
|
||||||
<section class="section section-problem" id="problem">
|
<section class="section section-problem" id="problem">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-label">The problem we keep seeing</div>
|
<div class="section-label">The problem we keep seeing</div>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ CATEGORY ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ CATEGORY / PILLARS ═══════════════════════ -->
|
||||||
<section class="section section-category" id="work">
|
<section class="section section-category" id="work">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-label">One discipline. Not five services.</div>
|
<div class="section-label">One discipline. Not five services.</div>
|
||||||
@@ -112,7 +112,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="pillars">
|
<div class="pillars">
|
||||||
<!-- PILLAR 1 -->
|
|
||||||
<div class="pillar">
|
<div class="pillar">
|
||||||
<div class="pillar-number">01</div>
|
<div class="pillar-number">01</div>
|
||||||
<h3 class="pillar-title">Infrastructure</h3>
|
<h3 class="pillar-title">Infrastructure</h3>
|
||||||
@@ -130,7 +129,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PILLAR 2 -->
|
|
||||||
<div class="pillar">
|
<div class="pillar">
|
||||||
<div class="pillar-number">02</div>
|
<div class="pillar-number">02</div>
|
||||||
<h3 class="pillar-title">Intelligence</h3>
|
<h3 class="pillar-title">Intelligence</h3>
|
||||||
@@ -148,7 +146,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PILLAR 3 -->
|
|
||||||
<div class="pillar">
|
<div class="pillar">
|
||||||
<div class="pillar-number">03</div>
|
<div class="pillar-number">03</div>
|
||||||
<h3 class="pillar-title">Independence</h3>
|
<h3 class="pillar-title">Independence</h3>
|
||||||
@@ -169,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ LIVE FEED ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ LIVE FEED ═══════════════════════ -->
|
||||||
<section class="section section-live" id="live">
|
<section class="section section-live" id="live">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="live-header">
|
<div class="live-header">
|
||||||
@@ -186,15 +183,15 @@
|
|||||||
|
|
||||||
<div class="live-stats">
|
<div class="live-stats">
|
||||||
<div class="live-stat">
|
<div class="live-stat">
|
||||||
<div class="live-stat-num" id="stat-shipped">90</div>
|
<div class="live-stat-num" id="stat-shipped">—</div>
|
||||||
<div class="live-stat-label">Systems shipped</div>
|
<div class="live-stat-label">Systems shipped</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-stat">
|
<div class="live-stat">
|
||||||
<div class="live-stat-num live-stat-green" id="stat-rate">98%</div>
|
<div class="live-stat-num live-stat-green" id="stat-rate">—</div>
|
||||||
<div class="live-stat-label">Ship rate</div>
|
<div class="live-stat-label">Ship rate</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-stat">
|
<div class="live-stat">
|
||||||
<div class="live-stat-num live-stat-amber" id="stat-oops">17</div>
|
<div class="live-stat-num live-stat-amber" id="stat-oops">—</div>
|
||||||
<div class="live-stat-label">Oops (transparent)</div>
|
<div class="live-stat-label">Oops (transparent)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-stat">
|
<div class="live-stat">
|
||||||
@@ -207,39 +204,10 @@
|
|||||||
<div class="live-feed-header">
|
<div class="live-feed-header">
|
||||||
<div class="live-pulse"></div>
|
<div class="live-pulse"></div>
|
||||||
<span>Live Engineering Feed</span>
|
<span>Live Engineering Feed</span>
|
||||||
<a href="https://calvana.quikcue.com/live" target="_blank" class="live-feed-link">Open full feed →</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="live-feed-entries" id="live-entries">
|
<div class="live-feed-entries" id="live-entries">
|
||||||
<!-- Populated by JS from calvana API or static snapshot -->
|
<div class="live-entry live-entry-loading">
|
||||||
<div class="live-entry">
|
<span class="live-entry-title" style="opacity:0.3">Loading live feed…</span>
|
||||||
<span class="live-entry-id">#96</span>
|
|
||||||
<span class="live-entry-badge badge-shipping">Shipping</span>
|
|
||||||
<span class="live-entry-title">QuikCue Homepage — New site alongside Calvana</span>
|
|
||||||
</div>
|
|
||||||
<div class="live-entry">
|
|
||||||
<span class="live-entry-id">#95</span>
|
|
||||||
<span class="live-entry-badge badge-shipped">Shipped</span>
|
|
||||||
<span class="live-entry-title">Self-healing deploy pipeline — auto-rollback on health check failure</span>
|
|
||||||
</div>
|
|
||||||
<div class="live-entry">
|
|
||||||
<span class="live-entry-id">#94</span>
|
|
||||||
<span class="live-entry-badge badge-shipped">Shipped</span>
|
|
||||||
<span class="live-entry-title">Calvana ship log — DB-backed persistence across sessions</span>
|
|
||||||
</div>
|
|
||||||
<div class="live-entry">
|
|
||||||
<span class="live-entry-id">#93</span>
|
|
||||||
<span class="live-entry-badge badge-shipped">Shipped</span>
|
|
||||||
<span class="live-entry-title">Remote command tool — run anything on any container</span>
|
|
||||||
</div>
|
|
||||||
<div class="live-entry">
|
|
||||||
<span class="live-entry-id">#92</span>
|
|
||||||
<span class="live-entry-badge badge-shipped">Shipped</span>
|
|
||||||
<span class="live-entry-title">Deploy tool — auto-discovers apps from Dokploy DB</span>
|
|
||||||
</div>
|
|
||||||
<div class="live-entry">
|
|
||||||
<span class="live-entry-id">#91</span>
|
|
||||||
<span class="live-entry-badge badge-shipped">Shipped</span>
|
|
||||||
<span class="live-entry-title">PNPL — Pledge Now Pay Later checkout deployed</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-feed-fade"></div>
|
<div class="live-feed-fade"></div>
|
||||||
@@ -258,7 +226,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ CLIENTS ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ CLIENTS ═══════════════════════ -->
|
||||||
<section class="section section-clients">
|
<section class="section section-clients">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-label">Who we build for</div>
|
<div class="section-label">Who we build for</div>
|
||||||
@@ -307,7 +275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ CTA ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ CTA ═══════════════════════ -->
|
||||||
<section class="section section-cta" id="contact">
|
<section class="section section-cta" id="contact">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="cta-block">
|
<div class="cta-block">
|
||||||
@@ -334,7 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ FOOTER ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ FOOTER ═══════════════════════ -->
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
@@ -343,7 +311,7 @@
|
|||||||
<span class="footer-tagline">We build the machine. You run the mission.</span>
|
<span class="footer-tagline">We build the machine. You run the mission.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://calvana.quikcue.com/live" target="_blank">Live Feed</a>
|
<a href="https://calvana.quikcue.com/live" target="_blank">Calvana</a>
|
||||||
<a href="https://git.quikcue.com" target="_blank">Git</a>
|
<a href="https://git.quikcue.com" target="_blank">Git</a>
|
||||||
<a href="mailto:omair@quikcue.com">Contact</a>
|
<a href="mailto:omair@quikcue.com">Contact</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,9 +323,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════ SCRIPTS ═══════════════════════════════════════════ -->
|
<!-- ═══════════════════════ SCRIPTS ═══════════════════════ -->
|
||||||
<script>
|
<script>
|
||||||
// Smooth scroll for anchor links
|
// ── Smooth scroll ──
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||||
a.addEventListener('click', e => {
|
a.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -366,7 +334,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Intersection Observer for fade-in animations
|
// ── Intersection Observer for fade-in ──
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
@@ -376,42 +344,113 @@
|
|||||||
});
|
});
|
||||||
}, { threshold: 0.1, rootMargin: '0px 0px -60px 0px' });
|
}, { threshold: 0.1, rootMargin: '0px 0px -60px 0px' });
|
||||||
|
|
||||||
document.querySelectorAll('.pillar, .client-card, .problem-stat, .live-stat, .live-entry').forEach(el => {
|
document.querySelectorAll('.pillar, .client-card, .problem-stat, .live-stat').forEach(el => {
|
||||||
el.classList.add('fade-in');
|
el.classList.add('fade-in');
|
||||||
observer.observe(el);
|
observer.observe(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate stat numbers on scroll
|
// ── Animate numbers ──
|
||||||
const animateValue = (el, start, end, duration) => {
|
const animateValue = (el, start, end, duration, suffix) => {
|
||||||
const range = end - start;
|
const range = end - start;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const step = (now) => {
|
const step = (now) => {
|
||||||
const progress = Math.min((now - startTime) / duration, 1);
|
const progress = Math.min((now - startTime) / duration, 1);
|
||||||
const eased = 1 - Math.pow(1 - progress, 3);
|
const eased = 1 - Math.pow(1 - progress, 3);
|
||||||
const val = Math.floor(start + range * eased);
|
el.textContent = Math.floor(start + range * eased) + (suffix || '');
|
||||||
el.textContent = val + (el.dataset.suffix || '');
|
|
||||||
if (progress < 1) requestAnimationFrame(step);
|
if (progress < 1) requestAnimationFrame(step);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(step);
|
requestAnimationFrame(step);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Live Feed: fetch from calvana API ──
|
||||||
|
const CALVANA_API = 'https://calvana.quikcue.com/api';
|
||||||
|
|
||||||
|
async function loadLiveFeed() {
|
||||||
|
try {
|
||||||
|
const [shipsRes, statsRes] = await Promise.all([
|
||||||
|
fetch(`${CALVANA_API}/ships?limit=12`),
|
||||||
|
fetch(`${CALVANA_API}/stats`)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (shipsRes.ok) {
|
||||||
|
const ships = await shipsRes.json();
|
||||||
|
renderShips(ships);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statsRes.ok) {
|
||||||
|
const stats = await statsRes.json();
|
||||||
|
renderStats(stats);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: render static data baked at build time
|
||||||
|
renderShips(FALLBACK_SHIPS);
|
||||||
|
renderStats(FALLBACK_STATS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback data (updated at deploy time)
|
||||||
|
const FALLBACK_STATS = { total: 92, shipped: 89, oops: 17 };
|
||||||
|
const FALLBACK_SHIPS = [
|
||||||
|
{ id: 98, title: "Milestone Recognition: n8n → Mailchimp via donation_warehouse", status: "shipping" },
|
||||||
|
{ id: 97, title: "QuikCue site — Git + Dokploy proper deployment", status: "shipping" },
|
||||||
|
{ id: 96, title: "QuikCue Homepage — Elite Rebrand of Calvana", status: "shipped" },
|
||||||
|
{ id: 95, title: "PNPL: Dashboard revamp — header, sidebar, upsell funnel", status: "shipped" },
|
||||||
|
{ id: 94, title: "PNPL: Mini embed widget + visible back button", status: "shipped" },
|
||||||
|
{ id: 93, title: "PNPL — WhatsApp banner redesign + Collect quick-add appeal picker", status: "shipped" },
|
||||||
|
{ id: 92, title: "PNPL — Conditional & match funding pledges", status: "shipped" },
|
||||||
|
{ id: 91, title: "Scheduled Giving data audit + junk filtering + error fixes", status: "shipped" },
|
||||||
|
{ id: 90, title: "PNPL — Fundamental Collect redesign: unified link creation", status: "shipped" },
|
||||||
|
{ id: 89, title: "Git org cleanup — CharityRight on GitLab, QuikCue on Gitea", status: "shipped" },
|
||||||
|
{ id: 88, title: "PNPL — Landing page philosophy across ALL dashboard pages", status: "shipped" },
|
||||||
|
{ id: 87, title: "Scheduled Giving Dashboard & Navigation Overhaul", status: "shipped" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderStats(stats) {
|
||||||
|
const shipped = stats.shipped || 0;
|
||||||
|
const total = stats.total || 0;
|
||||||
|
const oops = stats.oops || 0;
|
||||||
|
const rate = total > 0 ? Math.round((shipped / total) * 100) : 0;
|
||||||
|
|
||||||
|
const elShipped = document.getElementById('stat-shipped');
|
||||||
|
const elRate = document.getElementById('stat-rate');
|
||||||
|
const elOops = document.getElementById('stat-oops');
|
||||||
|
|
||||||
|
// Animate if visible, otherwise set directly
|
||||||
const statsObserver = new IntersectionObserver((entries) => {
|
const statsObserver = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const el = entry.target;
|
animateValue(elShipped, 0, shipped, 1200, '');
|
||||||
const text = el.textContent;
|
animateValue(elRate, 0, rate, 1200, '%');
|
||||||
const num = parseInt(text);
|
animateValue(elOops, 0, oops, 1200, '');
|
||||||
const suffix = text.replace(/\d+/, '');
|
statsObserver.disconnect();
|
||||||
if (!isNaN(num)) {
|
|
||||||
el.dataset.suffix = suffix;
|
|
||||||
animateValue(el, 0, num, 1200);
|
|
||||||
}
|
|
||||||
statsObserver.unobserve(el);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, { threshold: 0.5 });
|
}, { threshold: 0.5 });
|
||||||
|
statsObserver.observe(elShipped.parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.live-stat-num').forEach(el => statsObserver.observe(el));
|
function renderShips(ships) {
|
||||||
|
const container = document.getElementById('live-entries');
|
||||||
|
container.innerHTML = ships.map(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);
|
||||||
|
return `<div class="live-entry fade-in visible">
|
||||||
|
<span class="live-entry-id">#${s.id}</span>
|
||||||
|
<span class="live-entry-badge ${badgeClass}">${label}</span>
|
||||||
|
<span class="live-entry-title">${escapeHtml(s.title)}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load feed — try API first, fallback to baked data
|
||||||
|
loadLiveFeed();
|
||||||
|
|
||||||
// Live pulse animation
|
// Live pulse animation
|
||||||
const pulse = document.querySelector('.live-pulse');
|
const pulse = document.querySelector('.live-pulse');
|
||||||
|
|||||||
Reference in New Issue
Block a user