pngwn commited on
Commit
e96f49a
·
1 Parent(s): a8647ab

countdown

Browse files
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 StampBadge from './StampBadge.svelte';
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="hero__grid" style="margin-top:22px;align-items:flex-end">
 
 
 
 
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 style="display:flex;justify-content:flex-end">
53
- <div class="floaty"><StampBadge glyph="compass" accent="var(--forest)" size={150} /></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: 64px 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,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
- .hero__grid { grid-template-columns: 1fr; gap: 32px; }
 
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 ============ */