/quote redesign: CD's "letter" QOTD (deckle frame, wax-seal date, watermark)
Rebuilt /quote to CD's QOTD v3 — a warm letter card with a dashed deckle frame, an overlapping wax-seal date stamp, a giant faded quote-mark watermark, the quote in Playfair Display italic, attribution, a "What it means" note, and Copy/Share. Wired to live /api/quote/today (date → seal, text, author/work, meaning). Uses our HubShell (HubBar + Back + footer + account icon) per the brief. Self- hosted Playfair Display (roman+italic, OFL) for the signature quote/watermark/ seal; Newsreader for the body serif + Hanken for sans labels (kept to our stack to avoid font sprawl). Rose accent matches CD (#a4607a family). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,32 @@
|
||||
let q = $state(null);
|
||||
let state = $state('loading');
|
||||
|
||||
const MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
|
||||
let seal = $derived.by(() => {
|
||||
if (!q?.date) return null;
|
||||
const [, m, d] = q.date.split('-').map(Number);
|
||||
return MONTHS[m - 1] ? { mon: MONTHS[m - 1], day: d } : null;
|
||||
});
|
||||
|
||||
let copied = $state(false);
|
||||
async function copyQuote() {
|
||||
const attr = q?.author ? ` — ${q.author}${q.work ? ', ' + q.work : ''}` : '';
|
||||
try {
|
||||
await navigator.clipboard.writeText(`“${q.text}”${attr}`);
|
||||
copied = true; setTimeout(() => (copied = false), 1800);
|
||||
} catch { /* no clipboard — silent */ }
|
||||
}
|
||||
|
||||
let shared = $state(false);
|
||||
async function share() {
|
||||
const url = location.href;
|
||||
try {
|
||||
if (navigator.share) { await navigator.share({ title: 'Quote of the Day · upbeatBytes', text: `“${q.text}”`, url }); return; }
|
||||
await navigator.clipboard.writeText(url);
|
||||
shared = true; setTimeout(() => (shared = false), 1800);
|
||||
} catch { /* cancelled / unsupported */ }
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
q = await getJSON('/api/quote/today');
|
||||
@@ -24,22 +50,52 @@
|
||||
<HubShell active="">
|
||||
<article class="quote-page">
|
||||
{#if state === 'ready'}
|
||||
<p class="eyebrow">Quote of the day</p>
|
||||
<blockquote class="quote">
|
||||
<span class="glyph" aria-hidden="true">“</span>
|
||||
<p class="text">{q.text}</p>
|
||||
<footer class="attrib">
|
||||
<span class="dash"></span>
|
||||
<span class="by">{q.author}{#if q.work}, <span class="work">{q.work}</span>{/if}</span>
|
||||
</footer>
|
||||
</blockquote>
|
||||
<!-- CD's "letter" treatment: warm card, dashed deckle frame, wax-seal date, faded quote-mark watermark -->
|
||||
<div class="letter">
|
||||
{#if seal}
|
||||
<div class="seal" aria-hidden="true">
|
||||
<span class="seal-mon">{seal.mon}</span>
|
||||
<span class="seal-day">{seal.day}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="deckle">
|
||||
<span class="wm" aria-hidden="true">”</span>
|
||||
|
||||
{#if q.meaning}
|
||||
<section class="meaning">
|
||||
<h2>What it means</h2>
|
||||
<p>{q.meaning}</p>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="eyebrow">
|
||||
<span class="eye-rule"></span>
|
||||
<span class="eye-label">Quote of the Day</span>
|
||||
</div>
|
||||
|
||||
<p class="quote">{q.text}</p>
|
||||
|
||||
{#if q.author}
|
||||
<div class="attrib">
|
||||
<span class="at-rule"></span>
|
||||
<span class="at-by">{q.author}{#if q.work}, <span class="work">{q.work}</span>{/if}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if q.meaning}
|
||||
<div class="hr"></div>
|
||||
<div class="means-head">
|
||||
<span class="mh-rule"></span>
|
||||
<span class="mh-label">What it means</span>
|
||||
</div>
|
||||
<p class="meaning">{q.meaning}</p>
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
<button class="act act-copy" onclick={copyQuote}>
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
<button class="act act-share" onclick={share}>
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/></svg>
|
||||
{shared ? 'Link copied' : 'Share'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if state === 'empty'}
|
||||
<p class="note">Today's quote is on its way. Check back soon.</p>
|
||||
{:else}
|
||||
@@ -49,35 +105,73 @@
|
||||
</HubShell>
|
||||
|
||||
<style>
|
||||
.quote-page { max-width: 720px; margin: 0 auto; }
|
||||
.eyebrow {
|
||||
font-size: 12px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
|
||||
color: #b06a86; margin: clamp(8px, 3vw, 28px) 0 0; text-align: center;
|
||||
@font-face { font-family: 'Playfair Display'; src: url('/fonts/playfair-var.woff2') format('woff2'); font-weight: 500 700; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Playfair Display'; src: url('/fonts/playfair-italic-var.woff2') format('woff2'); font-weight: 500 700; font-style: italic; font-display: swap; }
|
||||
|
||||
.quote-page { max-width: 880px; margin: 0 auto; }
|
||||
|
||||
/* the card + its inner deckle frame */
|
||||
.letter {
|
||||
position: relative; margin-top: clamp(24px, 4vw, 40px); background: #f6ead0;
|
||||
border: 1px solid rgba(120, 90, 40, 0.10); border-radius: 16px;
|
||||
box-shadow: 0 26px 50px -32px rgba(90, 60, 30, 0.6); padding: clamp(12px, 1.6vw, 18px);
|
||||
}
|
||||
.quote { margin: clamp(18px, 4vw, 34px) 0 0; position: relative; text-align: center; }
|
||||
.glyph {
|
||||
font-family: 'Newsreader', Georgia, serif; font-size: clamp(4rem, 11vw, 7rem); line-height: 0.6;
|
||||
color: rgba(176, 106, 134, 0.22); display: block; height: clamp(2rem, 5vw, 3rem);
|
||||
.deckle {
|
||||
position: relative; border: 1.5px dashed rgba(170, 110, 135, 0.4); border-radius: 11px;
|
||||
padding: clamp(28px, 5vw, 40px) clamp(24px, 6vw, 56px) clamp(36px, 6vw, 54px); overflow: hidden;
|
||||
}
|
||||
.quote .text {
|
||||
font-family: 'Newsreader', Georgia, serif; font-style: italic; font-weight: 500;
|
||||
font-size: clamp(1.6rem, 4.5vw, 2.6rem); line-height: 1.28; letter-spacing: -0.01em;
|
||||
color: #2f2240; margin: 8px 0 0;
|
||||
/* giant faded closing-quote watermark, bleeding off the bottom-right */
|
||||
.wm {
|
||||
position: absolute; right: clamp(-20px, -2vw, -30px); bottom: clamp(-70px, -12vw, -120px);
|
||||
font-family: 'Playfair Display', Georgia, serif; font-size: clamp(220px, 46vw, 400px); line-height: 1;
|
||||
color: rgba(170, 110, 135, 0.08); pointer-events: none; user-select: none;
|
||||
}
|
||||
.attrib { display: flex; align-items: center; justify-content: center; gap: 12px; margin-top: clamp(22px, 4vw, 32px); }
|
||||
.dash { width: 30px; height: 1px; background: #c9a6ba; }
|
||||
.by { font-family: 'Newsreader', Georgia, serif; font-size: 1.1rem; color: #7c64a0; }
|
||||
|
||||
/* wax-seal date stamp, overlapping the top-right corner */
|
||||
.seal {
|
||||
position: absolute; top: -26px; right: clamp(20px, 6vw, 52px); z-index: 2;
|
||||
width: clamp(62px, 9vw, 74px); height: clamp(62px, 9vw, 74px); border-radius: 50%;
|
||||
background: #fbf3e3; border: 1.5px solid rgba(170, 110, 135, 0.55);
|
||||
box-shadow: 0 8px 20px -8px rgba(90, 60, 30, 0.5); transform: rotate(-8deg);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center; color: #a4607a;
|
||||
}
|
||||
.seal-mon { font-family: 'Hanken Grotesk', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: 0.18em; }
|
||||
.seal-day { font-family: 'Playfair Display', Georgia, serif; font-size: clamp(28px, 4vw, 33px); font-weight: 700; line-height: 0.82; }
|
||||
|
||||
.eyebrow { position: relative; display: flex; align-items: center; gap: 14px; }
|
||||
.eye-rule { width: 34px; height: 3px; background: #8f5e72; border-radius: 2px; flex: none; }
|
||||
.eye-label { font-family: 'Hanken Grotesk', sans-serif; font-size: clamp(13px, 1.7vw, 16px); font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: #7e4f63; }
|
||||
|
||||
.quote {
|
||||
position: relative; font-family: 'Playfair Display', Georgia, serif; font-style: italic; font-weight: 600;
|
||||
font-size: clamp(1.7rem, 5vw, 2.85rem); line-height: 1.18; letter-spacing: -0.01em; color: #3c2530;
|
||||
margin: clamp(18px, 3vw, 24px) 0 0; max-width: 34ch;
|
||||
}
|
||||
|
||||
.attrib { position: relative; display: flex; align-items: center; gap: 14px; margin-top: clamp(20px, 3vw, 26px); }
|
||||
.at-rule { width: 28px; height: 1.5px; background: #c79bac; flex: none; }
|
||||
.at-by { font-family: 'Newsreader', Georgia, serif; font-size: clamp(1rem, 1.6vw, 1.13rem); color: #8f5e72; }
|
||||
.work { font-style: italic; }
|
||||
|
||||
.hr { position: relative; height: 1px; background: rgba(120, 90, 40, 0.16); margin: clamp(28px, 4vw, 38px) 0 24px; }
|
||||
.means-head { position: relative; display: flex; align-items: center; gap: 12px; }
|
||||
.mh-rule { width: 26px; height: 2px; background: #bcae93; border-radius: 2px; flex: none; }
|
||||
.mh-label { font-family: 'Hanken Grotesk', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: 0.13em; text-transform: uppercase; color: #a89880; }
|
||||
.meaning {
|
||||
margin: clamp(36px, 6vw, 56px) auto 0; max-width: 600px;
|
||||
border-top: 1px solid #ecdce4; padding-top: 26px;
|
||||
position: relative; font-family: 'Newsreader', Georgia, serif; font-size: clamp(1rem, 1.8vw, 1.13rem);
|
||||
line-height: 1.65; color: #5c5249; margin: 12px 0 0; max-width: 60ch;
|
||||
}
|
||||
.meaning h2 {
|
||||
font-size: 12px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase;
|
||||
color: var(--muted); margin: 0 0 12px; text-align: center;
|
||||
|
||||
.actions { position: relative; display: flex; gap: 12px; margin-top: clamp(26px, 4vw, 32px); flex-wrap: wrap; }
|
||||
.act {
|
||||
display: inline-flex; align-items: center; gap: 7px; font-family: 'Hanken Grotesk', sans-serif;
|
||||
font-size: 14px; font-weight: 600; padding: 10px 18px; border-radius: 999px; cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent; transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.meaning p { font-size: clamp(1.05rem, 2vw, 1.2rem); line-height: 1.6; color: #4a4255; margin: 0; text-align: center; }
|
||||
.act-copy { background: #a4607a; color: #fff; border: none; }
|
||||
.act-copy:hover { background: #8f5165; }
|
||||
.act-share { background: transparent; color: #8f5e72; border: 1px solid #d3b3c0; }
|
||||
.act-share:hover { border-color: #a4607a; color: #a4607a; }
|
||||
|
||||
.note { text-align: center; color: var(--muted); font-size: 1.05rem; margin-top: 60px; }
|
||||
</style>
|
||||
|
||||
@@ -9,3 +9,5 @@ License: SIL Open Font License 1.1. All self-hosted (no Google hotlink) for the
|
||||
Work Sans — Wei Huang, OFL 1.1 (Google Fonts). Latin subset, variable 400–700.
|
||||
|
||||
Space Mono — Colophon Foundry, OFL 1.1 (Google Fonts). Latin subset, 400.
|
||||
|
||||
Playfair Display — Claus Eggers Sørensen, OFL 1.1 (Google Fonts). Latin subset, var 500–700 + italic. (QOTD)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user