Homepage hub: /home2 prototype — sections-as-data + bigger shell + hybrid room cards
- New sections registry (lib/rooms.js): each room is one data entry (title/blurb/href/cta/ size/preview/icon) — add or resize by editing the list. - Reusable RoomCard (lib/components/RoomCard.svelte) with size variants and hybrid previews: Art shows today's live thumbnail, News shows today's top headline, others are blurb+CTA. - /home2 hidden prototype (noindex, unlinked) with a bigger top bar (logo 50px, larger nav labels) and a reflowing grid hub of the four rooms (News/Art/Play/Daily Moment). Iterate the look here, then promote to / and remove the clone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<script>
|
||||
// A single hub "room" card. Presentational — the page passes any live preview data in.
|
||||
let { room, artImg = null, newsHeadline = '' } = $props();
|
||||
</script>
|
||||
|
||||
<a class="card card--{room.size}" class:live={room.preview !== 'static'} href={room.href}>
|
||||
{#if room.preview === 'art' && artImg}
|
||||
<div class="thumb" style="background-image:url({artImg})"></div>
|
||||
{/if}
|
||||
|
||||
<div class="body">
|
||||
<div class="eyebrow"><span class="ic">{room.icon}</span>{room.title}</div>
|
||||
|
||||
{#if room.preview === 'news' && newsHeadline}
|
||||
<p class="headline">“{newsHeadline}”</p>
|
||||
{:else}
|
||||
<p class="blurb">{room.blurb}</p>
|
||||
{/if}
|
||||
|
||||
<span class="cta">{room.cta} <span class="arr">→</span></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative; display: flex; flex-direction: column; justify-content: flex-end;
|
||||
overflow: hidden; text-decoration: none; color: var(--ink);
|
||||
background: var(--surface); border: 1px solid var(--line); border-radius: 18px;
|
||||
padding: clamp(18px, 2.2vw, 26px); min-height: 160px;
|
||||
box-shadow: 0 10px 30px rgba(20, 30, 45, 0.06);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.card:hover { transform: translateY(-4px); box-shadow: 0 20px 46px rgba(20, 30, 45, 0.12); border-color: #ddd4c4; }
|
||||
|
||||
/* size variants (grid spans set by the page) */
|
||||
.card--large { min-height: 340px; }
|
||||
.card--wide { min-height: 200px; }
|
||||
.card--tall { min-height: 340px; }
|
||||
.card--small { min-height: 160px; }
|
||||
|
||||
/* live art preview fills behind the body */
|
||||
.thumb {
|
||||
position: absolute; inset: 0; background-size: cover; background-position: center;
|
||||
}
|
||||
.thumb::after {
|
||||
content: ""; position: absolute; inset: 0;
|
||||
background: linear-gradient(to top, rgba(20, 24, 30, 0.78) 6%, rgba(20, 24, 30, 0.20) 46%, rgba(20, 24, 30, 0) 70%);
|
||||
}
|
||||
|
||||
.body { position: relative; z-index: 1; }
|
||||
.card.live:has(.thumb) .body { color: #fff; }
|
||||
|
||||
.eyebrow {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||
font-size: clamp(1.15rem, 2vw, 1.6rem); font-weight: 600; letter-spacing: -0.01em;
|
||||
}
|
||||
.ic { font-size: 1.1em; line-height: 1; }
|
||||
.card--large .eyebrow { font-size: clamp(1.5rem, 3vw, 2.1rem); }
|
||||
|
||||
.blurb { margin: 10px 0 16px; color: var(--muted); font-size: 1rem; line-height: 1.45; max-width: 42ch; }
|
||||
.headline {
|
||||
margin: 12px 0 16px; font-family: Georgia, serif; font-style: italic;
|
||||
font-size: clamp(1.05rem, 1.8vw, 1.35rem); line-height: 1.35; max-width: 40ch;
|
||||
}
|
||||
.card.live:has(.thumb) .blurb { color: rgba(255, 255, 255, 0.86); }
|
||||
|
||||
.cta { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; color: var(--accent); font-size: 0.98rem; }
|
||||
.card.live:has(.thumb) .cta { color: #fff; }
|
||||
.arr { transition: transform 0.18s ease; }
|
||||
.card:hover .arr { transform: translateX(4px); }
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
// The homepage hub as data. Each "room" is one entry — add, reorder, or resize by
|
||||
// editing this list. `size` drives the grid footprint (large | wide | tall | small);
|
||||
// `preview` picks how the card shows itself (news/art = live snippet, static = blurb).
|
||||
// Copy here is placeholder — owner-written final wording lands later.
|
||||
export const ROOMS = [
|
||||
{
|
||||
id: 'news',
|
||||
title: 'Good News',
|
||||
blurb: "The world's quiet good, gathered each morning.",
|
||||
href: '/', // becomes /news once the Brief is split out
|
||||
cta: 'Read the Brief',
|
||||
size: 'large',
|
||||
preview: 'news',
|
||||
icon: '📰',
|
||||
},
|
||||
{
|
||||
id: 'art',
|
||||
title: 'Daily Art',
|
||||
blurb: 'A masterwork a day, beautifully framed.',
|
||||
href: '/art',
|
||||
cta: 'View today',
|
||||
size: 'wide',
|
||||
preview: 'art',
|
||||
icon: '🖼️',
|
||||
},
|
||||
{
|
||||
id: 'play',
|
||||
title: 'Play',
|
||||
blurb: 'Daily Word, Word Search, Bloom, Memory Match.',
|
||||
href: '/play',
|
||||
cta: 'Enter',
|
||||
size: 'small',
|
||||
preview: 'static',
|
||||
icon: '🎲',
|
||||
},
|
||||
{
|
||||
id: 'moment',
|
||||
title: 'Daily Moment',
|
||||
blurb: 'A small calm to carry with you.',
|
||||
href: '#',
|
||||
cta: 'Soon',
|
||||
size: 'small',
|
||||
preview: 'static',
|
||||
icon: '🌿',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,122 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import { ROOMS } from '$lib/rooms.js';
|
||||
import RoomCard from '$lib/components/RoomCard.svelte';
|
||||
|
||||
// Hidden prototype of the new homepage hub (/home2). Iterate the look here, then
|
||||
// promote to / and remove this clone — same approach we used for /art.
|
||||
let artImg = $state(null);
|
||||
let newsHeadline = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
try { artImg = (await getJSON('/api/art/today'))?.image_url ?? null; } catch { /* card falls back to blurb */ }
|
||||
try {
|
||||
const b = await getJSON('/api/brief?limit=1');
|
||||
newsHeadline = b?.items?.[0]?.title ?? '';
|
||||
} catch { /* card falls back to blurb */ }
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>upbeatBytes — a calmer corner of the internet</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="description" content="A calmer corner of the internet: good news, daily art, small games, and little resets." />
|
||||
</svelte:head>
|
||||
|
||||
<div class="room">
|
||||
<header class="bar">
|
||||
<a class="brand" href="/home2" aria-label="upbeatBytes home">
|
||||
<img src="/logo.svg" alt="upbeatBytes" width="586" height="196" />
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="/">News</a>
|
||||
<a href="/play">Games</a>
|
||||
<a href="/art">Art</a>
|
||||
<a class="acct" href="/account" aria-label="Your account">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="8" r="3.3" />
|
||||
<path d="M5.5 19.2a6.5 6.5 0 0 1 13 0" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="hub">
|
||||
<div class="intro">
|
||||
<h1>A calmer corner of the internet.</h1>
|
||||
<p>Good news, daily art, small games, and little resets.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
{#each ROOMS as r (r.id)}
|
||||
<RoomCard room={r} {artImg} {newsHeadline} />
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="foot">upbeatBytes — no ads, no paywalls, no doomscrolling.</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.room {
|
||||
--canvas: #faf6ee; --surface: #ffffff; --ink: #232a31; --muted: #707b86;
|
||||
--line: #ece5d8; --accent: #0a93c0; --accent-deep: #066c8e;
|
||||
min-height: 100vh; background: var(--canvas); color: var(--ink);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
/* Bigger, more present top bar — logo + nav labels scaled up. */
|
||||
.bar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 20px clamp(18px, 5vw, 64px);
|
||||
max-width: 1180px; width: 100%; margin: 0 auto; box-sizing: border-box;
|
||||
}
|
||||
.brand { display: block; line-height: 0; }
|
||||
.brand img { height: 50px; width: auto; display: block; }
|
||||
.nav { display: flex; align-items: center; gap: clamp(18px, 3vw, 38px); }
|
||||
.nav a { color: var(--ink); text-decoration: none; font-weight: 600; font-size: 1.12rem; }
|
||||
.nav a:hover { color: var(--accent); }
|
||||
.acct {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 44px; height: 44px; border-radius: 50%; color: var(--muted);
|
||||
}
|
||||
.acct:hover { color: var(--accent); background: #eef6f9; }
|
||||
|
||||
.hub {
|
||||
flex: 1; width: 100%; max-width: 1180px; margin: 0 auto; box-sizing: border-box;
|
||||
padding: clamp(16px, 3vw, 40px) clamp(18px, 5vw, 64px) clamp(40px, 6vw, 72px);
|
||||
}
|
||||
.intro { text-align: center; margin: clamp(8px, 2vw, 22px) 0 clamp(28px, 4vw, 44px); }
|
||||
.intro h1 {
|
||||
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||
font-size: clamp(1.9rem, 4.5vw, 3rem); margin: 0; letter-spacing: -0.015em; line-height: 1.08;
|
||||
}
|
||||
.intro p { color: var(--muted); margin: 12px 0 0; font-size: clamp(1rem, 2vw, 1.2rem); }
|
||||
|
||||
.grid {
|
||||
display: grid; gap: clamp(14px, 1.8vw, 22px);
|
||||
grid-template-columns: repeat(4, 1fr); grid-auto-flow: dense;
|
||||
}
|
||||
/* size variants → grid spans (RoomCard owns min-heights) */
|
||||
:global(.grid .card--large) { grid-column: span 2; grid-row: span 2; }
|
||||
:global(.grid .card--wide) { grid-column: span 2; }
|
||||
:global(.grid .card--tall) { grid-row: span 2; }
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.grid { grid-template-columns: repeat(2, 1fr); }
|
||||
:global(.grid .card--large), :global(.grid .card--wide) { grid-column: span 2; }
|
||||
:global(.grid .card--large), :global(.grid .card--tall) { grid-row: auto; }
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
:global(.grid .card--large), :global(.grid .card--wide), :global(.grid .card--tall) { grid-column: span 1; }
|
||||
}
|
||||
|
||||
.foot {
|
||||
text-align: center; color: var(--muted); font-size: 0.86rem;
|
||||
padding: 28px 16px 40px; border-top: 1px solid var(--line); margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user