pngwn commited on
Commit ·
e96f49a
1
Parent(s): a8647ab
countdown
Browse files- src/lib/components/Countdown.svelte +180 -0
- src/lib/components/Hero.svelte +18 -18
- src/lib/data/content.ts +2 -0
- src/lib/styles/global.css +7 -2
src/lib/components/Countdown.svelte
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onMount } from 'svelte';
|
| 3 |
+
import { EVENT } from '$lib/data/content';
|
| 4 |
+
import Icon from './Icon.svelte';
|
| 5 |
+
|
| 6 |
+
// Final submission deadline — read as UTC from the content model.
|
| 7 |
+
const DEADLINE = new Date(EVENT.deadlineISO).getTime();
|
| 8 |
+
|
| 9 |
+
const pad = (n: number) => String(n).padStart(2, '0');
|
| 10 |
+
|
| 11 |
+
type Parts = { days: string; hrs: string; min: string; sec: string };
|
| 12 |
+
|
| 13 |
+
let parts = $state<Parts | null>(null);
|
| 14 |
+
let done = $state(false);
|
| 15 |
+
|
| 16 |
+
function tick(): boolean {
|
| 17 |
+
const ms = DEADLINE - Date.now();
|
| 18 |
+
if (ms <= 0) {
|
| 19 |
+
done = true;
|
| 20 |
+
parts = { days: '00', hrs: '00', min: '00', sec: '00' };
|
| 21 |
+
return false;
|
| 22 |
+
}
|
| 23 |
+
const s = Math.floor(ms / 1000);
|
| 24 |
+
parts = {
|
| 25 |
+
days: pad(Math.floor(s / 86400)),
|
| 26 |
+
hrs: pad(Math.floor((s % 86400) / 3600)),
|
| 27 |
+
min: pad(Math.floor((s % 3600) / 60)),
|
| 28 |
+
sec: pad(s % 60)
|
| 29 |
+
};
|
| 30 |
+
return true;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Only run on the client — the page is prerendered, so SSR renders the
|
| 34 |
+
// placeholder grid below and the real values pop in on hydration (no mismatch).
|
| 35 |
+
onMount(() => {
|
| 36 |
+
if (!tick()) return;
|
| 37 |
+
const id = setInterval(() => {
|
| 38 |
+
if (!tick()) clearInterval(id);
|
| 39 |
+
}, 1000);
|
| 40 |
+
return () => clearInterval(id);
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
const units = $derived([
|
| 44 |
+
{ k: 'days', v: parts?.days ?? '––' },
|
| 45 |
+
{ k: 'hrs', v: parts?.hrs ?? '––' },
|
| 46 |
+
{ k: 'min', v: parts?.min ?? '––' },
|
| 47 |
+
{ k: 'sec', v: parts?.sec ?? '––' }
|
| 48 |
+
]);
|
| 49 |
+
</script>
|
| 50 |
+
|
| 51 |
+
<div class="countdown" class:countdown--done={done}>
|
| 52 |
+
<div class="countdown__head">
|
| 53 |
+
<span class="eyebrow">{done ? 'Deadline passed' : 'Counting down to'}</span>
|
| 54 |
+
<span class="countdown__when">Jun 15 · 23:59 UTC</span>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
{#if done}
|
| 58 |
+
<p class="countdown__closed">Submissions closed</p>
|
| 59 |
+
{:else}
|
| 60 |
+
<div class="countdown__grid" role="timer" aria-label="Time remaining until the submission deadline">
|
| 61 |
+
{#each units as u (u.k)}
|
| 62 |
+
<div class="countdown__cell" class:is-live={u.k === 'sec'}>
|
| 63 |
+
<b class="countdown__num">{u.v}</b>
|
| 64 |
+
<span class="countdown__lbl">{u.k}</span>
|
| 65 |
+
</div>
|
| 66 |
+
{/each}
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="countdown__foot">
|
| 70 |
+
<span class="countdown__note">Built something? Ship it before the clock runs out.</span>
|
| 71 |
+
<a class="btn btn--accent countdown__cta" href="/submit">
|
| 72 |
+
Submit your build <Icon name="arrow" size={18} stroke="#fff" />
|
| 73 |
+
</a>
|
| 74 |
+
</div>
|
| 75 |
+
{/if}
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<style>
|
| 79 |
+
.countdown {
|
| 80 |
+
width: 100%;
|
| 81 |
+
background: var(--kraft);
|
| 82 |
+
border: 2px solid var(--ink);
|
| 83 |
+
border-radius: 0;
|
| 84 |
+
box-shadow: 8px 8px 0 var(--ink);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.countdown__head {
|
| 88 |
+
display: flex;
|
| 89 |
+
align-items: baseline;
|
| 90 |
+
justify-content: space-between;
|
| 91 |
+
gap: 12px;
|
| 92 |
+
flex-wrap: wrap;
|
| 93 |
+
padding: 14px 20px;
|
| 94 |
+
border-bottom: 2px solid var(--ink);
|
| 95 |
+
}
|
| 96 |
+
.countdown__when {
|
| 97 |
+
font-family: var(--font-mono);
|
| 98 |
+
font-size: 12px;
|
| 99 |
+
letter-spacing: 0.06em;
|
| 100 |
+
text-transform: uppercase;
|
| 101 |
+
color: var(--ink-2);
|
| 102 |
+
white-space: nowrap;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.countdown__grid {
|
| 106 |
+
display: grid;
|
| 107 |
+
grid-template-columns: repeat(4, 1fr);
|
| 108 |
+
}
|
| 109 |
+
.countdown__cell {
|
| 110 |
+
padding: 22px 8px 18px;
|
| 111 |
+
text-align: center;
|
| 112 |
+
border-right: 2px solid var(--ink);
|
| 113 |
+
}
|
| 114 |
+
.countdown__cell:last-child {
|
| 115 |
+
border-right: none;
|
| 116 |
+
}
|
| 117 |
+
.countdown__num {
|
| 118 |
+
display: block;
|
| 119 |
+
font-family: var(--font-display);
|
| 120 |
+
font-weight: 800;
|
| 121 |
+
font-stretch: 115%;
|
| 122 |
+
font-variant-numeric: tabular-nums;
|
| 123 |
+
font-size: clamp(46px, 8vw, 96px);
|
| 124 |
+
line-height: 0.92;
|
| 125 |
+
letter-spacing: -0.02em;
|
| 126 |
+
color: var(--ink);
|
| 127 |
+
}
|
| 128 |
+
.countdown__cell.is-live .countdown__num {
|
| 129 |
+
color: var(--clay);
|
| 130 |
+
}
|
| 131 |
+
.countdown__lbl {
|
| 132 |
+
display: block;
|
| 133 |
+
margin-top: 10px;
|
| 134 |
+
font-family: var(--font-mono);
|
| 135 |
+
font-size: 11px;
|
| 136 |
+
letter-spacing: 0.16em;
|
| 137 |
+
text-transform: uppercase;
|
| 138 |
+
color: var(--ink-soft);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* action bar — guides users straight to submitting */
|
| 142 |
+
.countdown__foot {
|
| 143 |
+
display: flex;
|
| 144 |
+
align-items: center;
|
| 145 |
+
justify-content: space-between;
|
| 146 |
+
gap: 14px 24px;
|
| 147 |
+
flex-wrap: wrap;
|
| 148 |
+
padding: 16px 20px;
|
| 149 |
+
border-top: 2px solid var(--ink);
|
| 150 |
+
background: color-mix(in srgb, var(--amber) 12%, var(--kraft));
|
| 151 |
+
}
|
| 152 |
+
.countdown__note {
|
| 153 |
+
font-family: var(--font-mono);
|
| 154 |
+
font-size: 13px;
|
| 155 |
+
letter-spacing: 0.01em;
|
| 156 |
+
color: var(--ink-2);
|
| 157 |
+
max-width: 42ch;
|
| 158 |
+
}
|
| 159 |
+
.countdown__cta {
|
| 160 |
+
white-space: nowrap;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.countdown__closed {
|
| 164 |
+
margin: 0;
|
| 165 |
+
padding: 34px 20px;
|
| 166 |
+
text-align: center;
|
| 167 |
+
font-family: var(--font-display);
|
| 168 |
+
font-weight: 800;
|
| 169 |
+
font-stretch: 110%;
|
| 170 |
+
font-size: clamp(24px, 3.2vw, 34px);
|
| 171 |
+
color: var(--ink);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
@media (max-width: 560px) {
|
| 175 |
+
.countdown__cta {
|
| 176 |
+
width: 100%;
|
| 177 |
+
justify-content: center;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
</style>
|
src/lib/components/Hero.svelte
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { EVENT } from '$lib/data/content';
|
| 3 |
-
import
|
| 4 |
import TopoBackdrop from './TopoBackdrop.svelte';
|
| 5 |
import Icon from './Icon.svelte';
|
| 6 |
|
| 7 |
const stats = [
|
| 8 |
-
{ b: EVENT.deadline, s: 'final deadline' },
|
| 9 |
{ b: `≤ ${EVENT.paramCap}`, s: 'params, max' },
|
| 10 |
{ b: `${EVENT.pool}+`, s: 'prize pool' },
|
| 11 |
{ b: String(EVENT.waysToWin), s: 'ways to win' }
|
|
@@ -22,7 +21,11 @@
|
|
| 22 |
|
| 23 |
<h1 class="display" style="margin-top:18px">BUILD SMALL</h1>
|
| 24 |
|
| 25 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
<div>
|
| 27 |
<p
|
| 28 |
class="lede"
|
|
@@ -35,22 +38,19 @@
|
|
| 35 |
need to enter cleanly: the rules, the {EVENT.waysToWin} ways to win, and the right kit for what
|
| 36 |
you’re building.
|
| 37 |
</p>
|
| 38 |
-
<div style="margin-top:26px;display:flex;flex-direction:column;gap:20px;align-items:flex-start">
|
| 39 |
-
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
| 40 |
-
<a class="btn btn--accent" href="#prizes">
|
| 41 |
-
See the prizes <Icon name="arrow" size={18} stroke="#fff" />
|
| 42 |
-
</a>
|
| 43 |
-
<a class="btn btn--ghost" href="#recommender">Resources</a>
|
| 44 |
-
</div>
|
| 45 |
-
<div class="hero__statline">
|
| 46 |
-
{#each stats as st (st.s)}
|
| 47 |
-
<div class="hero__stat"><b>{st.b}</b><span>{st.s}</span></div>
|
| 48 |
-
{/each}
|
| 49 |
-
</div>
|
| 50 |
-
</div>
|
| 51 |
</div>
|
| 52 |
-
<div
|
| 53 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
</div>
|
| 55 |
</div>
|
| 56 |
</div>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { EVENT } from '$lib/data/content';
|
| 3 |
+
import Countdown from './Countdown.svelte';
|
| 4 |
import TopoBackdrop from './TopoBackdrop.svelte';
|
| 5 |
import Icon from './Icon.svelte';
|
| 6 |
|
| 7 |
const stats = [
|
|
|
|
| 8 |
{ b: `≤ ${EVENT.paramCap}`, s: 'params, max' },
|
| 9 |
{ b: `${EVENT.pool}+`, s: 'prize pool' },
|
| 10 |
{ b: String(EVENT.waysToWin), s: 'ways to win' }
|
|
|
|
| 21 |
|
| 22 |
<h1 class="display" style="margin-top:18px">BUILD SMALL</h1>
|
| 23 |
|
| 24 |
+
<div class="hero__countdown">
|
| 25 |
+
<Countdown />
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="hero__grid" style="margin-top:34px;align-items:flex-end">
|
| 29 |
<div>
|
| 30 |
<p
|
| 31 |
class="lede"
|
|
|
|
| 38 |
need to enter cleanly: the rules, the {EVENT.waysToWin} ways to win, and the right kit for what
|
| 39 |
you’re building.
|
| 40 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
</div>
|
| 42 |
+
<div class="hero__aside">
|
| 43 |
+
<div style="display:flex;gap:12px;flex-wrap:wrap">
|
| 44 |
+
<a class="btn btn--accent" href="#prizes">
|
| 45 |
+
See the prizes <Icon name="arrow" size={18} stroke="#fff" />
|
| 46 |
+
</a>
|
| 47 |
+
<a class="btn btn--ghost" href="#recommender">Resources</a>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="hero__statline">
|
| 50 |
+
{#each stats as st (st.s)}
|
| 51 |
+
<div class="hero__stat"><b>{st.b}</b><span>{st.s}</span></div>
|
| 52 |
+
{/each}
|
| 53 |
+
</div>
|
| 54 |
</div>
|
| 55 |
</div>
|
| 56 |
</div>
|
src/lib/data/content.ts
CHANGED
|
@@ -20,6 +20,8 @@ export const EVENT = {
|
|
| 20 |
name: 'Build Small',
|
| 21 |
tagline: 'Build something small, local, and yours.',
|
| 22 |
deadline: 'June 15, 2026',
|
|
|
|
|
|
|
| 23 |
deadlineNote: 'final submission',
|
| 24 |
org: 'huggingface.co/BuildSmall',
|
| 25 |
paramCap: '32B',
|
|
|
|
| 20 |
name: 'Build Small',
|
| 21 |
tagline: 'Build something small, local, and yours.',
|
| 22 |
deadline: 'June 15, 2026',
|
| 23 |
+
// machine-readable deadline (UTC) — drives the live hero countdown
|
| 24 |
+
deadlineISO: '2026-06-15T23:59:00Z',
|
| 25 |
deadlineNote: 'final submission',
|
| 26 |
org: 'huggingface.co/BuildSmall',
|
| 27 |
paramCap: '32B',
|
src/lib/styles/global.css
CHANGED
|
@@ -319,7 +319,7 @@ h1,h2,h3,h4 { font-family: var(--font-display); margin: 0; line-height: 1.02; }
|
|
| 319 |
.stamp--spin:hover svg.stamp__ring { transform: rotate(28deg); }
|
| 320 |
|
| 321 |
/* ============ hero ============ */
|
| 322 |
-
.hero { position: relative; overflow: clip; padding:
|
| 323 |
.hero__grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 48px; align-items: center; }
|
| 324 |
.hero h1 { font-size: clamp(58px, 11vw, 150px); }
|
| 325 |
.hero__statline { display: flex; gap: 0; flex-wrap: wrap; border: 2px solid var(--ink);
|
|
@@ -334,9 +334,14 @@ h1,h2,h3,h4 { font-family: var(--font-display); margin: 0; line-height: 1.02; }
|
|
| 334 |
background: var(--kraft); padding: 28px; overflow: hidden;
|
| 335 |
box-shadow: 8px 8px 0 var(--ink);
|
| 336 |
}
|
|
|
|
|
|
|
|
|
|
| 337 |
@media (max-width: 900px){
|
| 338 |
-
.
|
|
|
|
| 339 |
.hero h1 { font-size: clamp(54px, 17vw, 96px); }
|
|
|
|
| 340 |
}
|
| 341 |
|
| 342 |
/* ============ tracks ============ */
|
|
|
|
| 319 |
.stamp--spin:hover svg.stamp__ring { transform: rotate(28deg); }
|
| 320 |
|
| 321 |
/* ============ hero ============ */
|
| 322 |
+
.hero { position: relative; overflow: clip; padding: 48px 0 84px; }
|
| 323 |
.hero__grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 48px; align-items: center; }
|
| 324 |
.hero h1 { font-size: clamp(58px, 11vw, 150px); }
|
| 325 |
.hero__statline { display: flex; gap: 0; flex-wrap: wrap; border: 2px solid var(--ink);
|
|
|
|
| 334 |
background: var(--kraft); padding: 28px; overflow: hidden;
|
| 335 |
box-shadow: 8px 8px 0 var(--ink);
|
| 336 |
}
|
| 337 |
+
/* countdown is the hero centrepiece — a full-width band under the headline */
|
| 338 |
+
.hero__countdown { width: 100%; margin-top: 30px; }
|
| 339 |
+
.hero__aside { display: flex; flex-direction: column; gap: 20px; align-items: flex-start; }
|
| 340 |
@media (max-width: 900px){
|
| 341 |
+
.hero { padding-top: 32px; }
|
| 342 |
+
.hero__grid { grid-template-columns: 1fr; gap: 28px; }
|
| 343 |
.hero h1 { font-size: clamp(54px, 17vw, 96px); }
|
| 344 |
+
.hero__countdown { margin-top: 24px; }
|
| 345 |
}
|
| 346 |
|
| 347 |
/* ============ tracks ============ */
|