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:
@@ -8,6 +8,19 @@
|
||||
let news = $state(null); // {id, title, summary, image}
|
||||
let art = $state(null); // {title, artist, year, image}
|
||||
let newsFit = $state('cover'); // 'cover' = full-bleed photo; 'contain' = framed-plate figure
|
||||
let word = $state(null); // /api/word/today
|
||||
let quote = $state(null); // /api/quote/today
|
||||
let fact = $state(null); // /api/onthisday/today
|
||||
|
||||
// small-joys display helpers
|
||||
const POS = { noun: 'n.', adjective: 'adj.', verb: 'v.', adverb: 'adv.', pronoun: 'pron.',
|
||||
preposition: 'prep.', conjunction: 'conj.', interjection: 'interj.' };
|
||||
const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : '');
|
||||
const clip = (s, n) => {
|
||||
if (!s || s.length <= n) return s || '';
|
||||
const cut = s.slice(0, n), i = cut.lastIndexOf(' ');
|
||||
return (i > 0 ? cut.slice(0, i) : cut).replace(/[\s,;:.]+$/, '') + '…';
|
||||
};
|
||||
|
||||
// truncation handled by CSS (-webkit-line-clamp:2) — breaks on whole words, fills 2 full lines
|
||||
let headline = $derived(news?.title ?? 'What went right this week: the good news that actually matters');
|
||||
@@ -43,6 +56,11 @@
|
||||
probe.src = news.image;
|
||||
}
|
||||
} catch { /* fall back to design copy */ }
|
||||
|
||||
// small joys (each falls back to its placeholder if the engine has nothing yet)
|
||||
try { word = await getJSON('/api/word/today'); } catch { /* placeholder */ }
|
||||
try { quote = await getJSON('/api/quote/today'); } catch { /* placeholder */ }
|
||||
try { fact = await getJSON('/api/onthisday/today'); } catch { /* placeholder */ }
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -54,35 +72,32 @@
|
||||
|
||||
{#snippet joyCard(i)}
|
||||
{#if i === 0}
|
||||
<div class="joy joy-word">
|
||||
<span class="wm" aria-hidden="true">S</span>
|
||||
<a class="joy joy-word" href="/word">
|
||||
<span class="wm" aria-hidden="true">{word ? cap(word.word)[0] : 'S'}</span>
|
||||
<div class="joy-in">
|
||||
<div class="tag"><span class="rule"></span><span class="tag-label">Word of the day</span></div>
|
||||
<p class="word-line"><span class="word">Serene</span> <span class="word-pos">adj.</span></p>
|
||||
<p class="word-pron">/səˈriːn/</p>
|
||||
<p class="def">Calm, peaceful, and untroubled. The quiet after a storm passes.</p>
|
||||
<p class="word-line"><span class="word">{word ? cap(word.word) : 'Serene'}</span> <span class="word-pos">{word ? (POS[word.part_of_speech] ?? word.part_of_speech ?? '') : 'adj.'}</span></p>
|
||||
<p class="word-pron">{word?.phonetic ?? '/səˈriːn/'}</p>
|
||||
<p class="def">{word ? clip(word.definition, 78) : 'Calm, peaceful, and untroubled. The quiet after a storm passes.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{:else if i === 1}
|
||||
<div class="joy joy-quote">
|
||||
<a class="joy joy-quote" href="/quote">
|
||||
<span class="wm wm-q" aria-hidden="true">“</span>
|
||||
<div class="joy-in">
|
||||
<div class="tag"><span class="rule"></span><span class="tag-label">Quote of the day</span></div>
|
||||
<p class="quote">Very little is needed to make a happy life.</p>
|
||||
<div class="attrib"><span class="attrib-rule"></span><span class="attrib-by">Marcus Aurelius</span></div>
|
||||
<p class="quote">{quote ? clip(quote.text, 92) : 'Very little is needed to make a happy life.'}</p>
|
||||
<div class="attrib"><span class="attrib-rule"></span><span class="attrib-by">{quote?.author ?? 'Marcus Aurelius'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="joy joy-fact">
|
||||
<a class="joy joy-fact" href="/onthisday">
|
||||
<div class="joy-in">
|
||||
<div class="joy-top">
|
||||
<div class="tag"><span class="rule"></span><span class="tag-label">A good thing today</span></div>
|
||||
<span class="soon">SOON</span>
|
||||
</div>
|
||||
<p class="fact-hero"><span class="year">1928</span> <span class="onthis">ON THIS DAY</span></p>
|
||||
<p class="fact">Penicillin was discovered by a happy accident.</p>
|
||||
<div class="tag"><span class="rule"></span><span class="tag-label">A good thing today</span></div>
|
||||
<p class="fact-hero"><span class="year">{fact?.year ?? '1928'}</span> <span class="onthis">ON THIS DAY</span></p>
|
||||
<p class="fact">{fact ? clip(fact.text, 96) : 'Penicillin was discovered by a happy accident.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -430,7 +445,12 @@
|
||||
.joys { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
/* all cells share one compact height (tight, not crowded) so every rotation matches */
|
||||
/* clamp every card to the WORD card's natural height (its tallest) so rotations never jump */
|
||||
.joy { position: relative; overflow: hidden; border-radius: 20px; padding: 18px 22px; min-height: 170px; box-sizing: border-box; }
|
||||
.joy {
|
||||
position: relative; overflow: hidden; border-radius: 20px; padding: 18px 22px; min-height: 170px;
|
||||
box-sizing: border-box; display: block; text-decoration: none; color: inherit;
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
.joy:hover { transform: translateY(-2px); }
|
||||
.joy-in { position: relative; } /* content sits above the watermark */
|
||||
.wm { position: absolute; font-family: 'Newsreader', Georgia, serif; line-height: 1; pointer-events: none; }
|
||||
|
||||
@@ -456,8 +476,6 @@
|
||||
.attrib-rule { width: 22px; height: 1px; background: #d8afc1; }
|
||||
.attrib-by { font-family: 'Newsreader', Georgia, serif; font-size: 13px; color: #97667f; }
|
||||
|
||||
.joy-fact .joy-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.joy-fact .soon { color: #b06a45; }
|
||||
.fact-hero { display: flex; align-items: baseline; gap: 8px; margin: 12px 0 0; }
|
||||
.year { font-family: 'Newsreader', Georgia, serif; font-weight: 500; font-size: 30px; color: #7a4a30; line-height: 0.9; }
|
||||
.onthis { font-size: 11px; color: #9e7a64; letter-spacing: 0.04em; }
|
||||
|
||||
Reference in New Issue
Block a user