Small joys: wire homepage rail to live data + rich pages (/word /quote /onthisday) + admin
- /home3: small-joys rail now reads live /api/word|quote|onthisday/today (placeholders only
as fallback); each cell links to its detail page.
- HubShell component (shared bar/footer/fonts/tokens) for the hub + detail pages.
- /word: big word, IPA, Listen (cached clip + browser-TTS fallback), definition, sentences.
- /quote: the quote, attribution, and the AI "what it means".
- /onthisday: the date, year + fact, image, summary, source.
- Admin "Small Joys" tab: per-pool list with feature/block/delete/add + re-pick, for all
three kinds. New admin API: GET/POST /api/admin/joys/{kind}[/{id}|/add|/repick].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import HubShell from '$lib/components/HubShell.svelte';
|
||||
|
||||
let f = $state(null);
|
||||
let state = $state('loading');
|
||||
|
||||
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
function dayLabel(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const [, m, d] = dateStr.split('-').map(Number);
|
||||
return `${MONTHS[m - 1]} ${d}`;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
f = await getJSON('/api/onthisday/today');
|
||||
state = f ? 'ready' : 'empty';
|
||||
} catch {
|
||||
state = 'empty';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>On This Day · upbeatBytes</title>
|
||||
<meta name="description" content="A good thing that happened on today's date in history." />
|
||||
</svelte:head>
|
||||
|
||||
<HubShell active="">
|
||||
<article class="otd-page">
|
||||
{#if state === 'ready'}
|
||||
<p class="eyebrow">A good thing today</p>
|
||||
<h1 class="date">{dayLabel(f.date)}</h1>
|
||||
|
||||
<div class="event">
|
||||
<p class="year-line"><span class="year">{f.year}</span><span class="ago">on this day in history</span></p>
|
||||
<p class="fact">{f.text}</p>
|
||||
</div>
|
||||
|
||||
{#if f.image_url}
|
||||
<div class="photo" style="background-image:url({f.image_url})"></div>
|
||||
{/if}
|
||||
|
||||
{#if f.summary}
|
||||
<p class="summary">{f.summary}</p>
|
||||
{/if}
|
||||
|
||||
{#if f.source_url}
|
||||
<a class="source" href={f.source_url} target="_blank" rel="noopener">Read more on Wikipedia →</a>
|
||||
{/if}
|
||||
{:else if state === 'empty'}
|
||||
<p class="note">Today's moment in history is on its way. Check back soon.</p>
|
||||
{:else}
|
||||
<p class="note">Finding today's good thing…</p>
|
||||
{/if}
|
||||
</article>
|
||||
</HubShell>
|
||||
|
||||
<style>
|
||||
.otd-page { max-width: 680px; margin: 0 auto; text-align: center; }
|
||||
.eyebrow {
|
||||
font-size: 12px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: #3f9a66; margin: clamp(8px, 3vw, 28px) 0 0;
|
||||
}
|
||||
.date {
|
||||
font-family: 'Newsreader', Georgia, serif; font-weight: 500; letter-spacing: -0.015em;
|
||||
font-size: clamp(2.4rem, 7vw, 4rem); line-height: 1; margin: 12px 0 0; color: var(--ink);
|
||||
}
|
||||
.event { margin-top: clamp(22px, 4vw, 34px); }
|
||||
.year-line { display: flex; align-items: baseline; justify-content: center; gap: 12px; margin: 0; }
|
||||
.year { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: clamp(1.8rem, 4vw, 2.6rem); color: #1e5b3b; line-height: 1; }
|
||||
.ago { font-size: 0.9rem; letter-spacing: 0.04em; color: #6f9683; text-transform: uppercase; }
|
||||
.fact {
|
||||
font-family: 'Newsreader', Georgia, serif; font-size: clamp(1.3rem, 3vw, 1.7rem); line-height: 1.34;
|
||||
color: #214a35; margin: 14px 0 0;
|
||||
}
|
||||
|
||||
.photo {
|
||||
margin: clamp(26px, 5vw, 40px) auto 0; max-width: 460px; aspect-ratio: 3/2;
|
||||
background-size: cover; background-position: center; border-radius: 16px;
|
||||
border: 1px solid #d8e6da; box-shadow: 0 10px 30px -16px rgba(40, 120, 75, 0.4);
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin: clamp(26px, 5vw, 38px) auto 0; max-width: 600px; text-align: left;
|
||||
font-size: 1.05rem; line-height: 1.65; color: #4a5a50;
|
||||
}
|
||||
|
||||
.source {
|
||||
display: inline-block; margin-top: clamp(24px, 4vw, 34px);
|
||||
font-size: 0.95rem; font-weight: 600; color: #3f9a66; text-decoration: none;
|
||||
}
|
||||
.source:hover { text-decoration: underline; }
|
||||
|
||||
.note { text-align: center; color: var(--muted); font-size: 1.05rem; margin-top: 60px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user