Session 27: PWA autopilot — NetworkFirst cache policy, stale bucket cleanup, offline fallback, push helper, manifest polish, tier fix (1584 tests)
This commit is contained in:
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
|
||||
template: '%s · VYNDR',
|
||||
},
|
||||
description:
|
||||
"Grade NBA, MLB, WNBA, and soccer props with intelligence the books don't want you to have. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
|
||||
"Grade your props across every sport with intelligence the books don't want you to have. NBA, MLB, WNBA, and soccer today — NFL and more through 2026. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
|
||||
applicationName: 'VYNDR',
|
||||
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
|
||||
manifest: '/manifest.json',
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Offline fallback (Session 27).
|
||||
*
|
||||
* Served by the service worker's navigation handler when a page request
|
||||
* fails on the network AND misses the runtime cache. Pre-cached on SW
|
||||
* install so it's always available. Kept dependency-free so it renders
|
||||
* with zero network.
|
||||
*/
|
||||
export default function OfflinePage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'var(--bg-0, #06060B)',
|
||||
color: 'var(--text-0, #F0F0F5)',
|
||||
padding: 24,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--grade-a, #00D4A0)',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
VYNDR
|
||||
</div>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 800, marginBottom: 8, letterSpacing: '-0.02em' }}>
|
||||
You're offline
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: 'var(--text-secondary, #8A8A9A)',
|
||||
maxWidth: 400,
|
||||
marginBottom: 24,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Scores and grades will refresh the moment you reconnect. Anything you
|
||||
loaded earlier is still cached and available.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
background: 'var(--grade-a, #00D4A0)',
|
||||
color: '#06060B',
|
||||
border: 'none',
|
||||
borderRadius: 6,
|
||||
fontWeight: 700,
|
||||
fontSize: 14,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -85,8 +85,11 @@ export default function ProfilePage() {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 16 }}>
|
||||
<div>
|
||||
<p className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', letterSpacing: '0.08em' }}>YOUR TIER</p>
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, marginTop: 4, textTransform: 'capitalize', color: tierColor(profile.tier) }}>
|
||||
{profile.tier}
|
||||
{/* Session 27 — always render a tier label. When the profile
|
||||
API returns null/undefined tier (free users sometimes do),
|
||||
fall back to 'free' so the field is never blank. */}
|
||||
<h2 style={{ fontSize: 28, fontWeight: 800, marginTop: 4, textTransform: 'capitalize', color: tierColor(profile.tier || 'free') }}>
|
||||
{profile.tier || 'free'}
|
||||
{profile.founder_pricing && (
|
||||
<span
|
||||
className="mono"
|
||||
|
||||
Reference in New Issue
Block a user