create-trokky 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/files-text.js +1 -1
- package/dist/manifest.d.ts +10 -1
- package/files/content/conference/src/trokky/schemas.ts +179 -0
- package/files/content/conference/src/trokky/seed.ts +276 -0
- package/files/content/conference/src/trokky/structure.ts +18 -0
- package/files/site/conference/src/components/Avatar.astro +27 -0
- package/files/site/conference/src/components/SessionCard.astro +27 -0
- package/files/site/conference/src/components/SpeakerCard.astro +12 -0
- package/files/site/conference/src/components/SponsorMark.astro +16 -0
- package/files/site/conference/src/layouts/Base.astro +356 -0
- package/files/site/conference/src/pages/index.astro +82 -0
- package/files/site/conference/src/pages/schedule.astro +65 -0
- package/files/site/conference/src/pages/sessions/[id].astro +57 -0
- package/files/site/conference/src/pages/sessions/index.astro +62 -0
- package/files/site/conference/src/pages/speakers/[id].astro +32 -0
- package/files/site/conference/src/pages/speakers/index.astro +20 -0
- package/files/site/conference/src/pages/sponsors.astro +37 -0
- package/files/site/conference/src/pages/venue.astro +73 -0
- package/files/site/conference/src/trokky/site.ts +182 -0
- package/manifest.json +26 -4
- package/package.json +2 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* All the chrome: fonts, the whole stylesheet, the header from the event's navigation and the
|
|
4
|
+
* footer from the event — nothing else knows this much of the design. The pages pass the event
|
|
5
|
+
* down and stay focused on their own body.
|
|
6
|
+
*
|
|
7
|
+
* One deliberate extension of the demo CSS: the filter bar also styles `a` alongside `button`,
|
|
8
|
+
* because the session filters are links that carry URL params, not forms.
|
|
9
|
+
*/
|
|
10
|
+
import { formatDateRange } from '../trokky/site'
|
|
11
|
+
interface Props { title?: string; description?: string; event: Record<string, any> | null }
|
|
12
|
+
const { title, description, event } = Astro.props
|
|
13
|
+
const ev = event
|
|
14
|
+
const siteName = ev?.name ?? 'Conference'
|
|
15
|
+
const nav: { label: string; href: string }[] = ev?.navigation ?? []
|
|
16
|
+
// Active link: the current path under the nav href (/speakers/speaker-01 sits under /speakers).
|
|
17
|
+
const current = Astro.url.pathname
|
|
18
|
+
---
|
|
19
|
+
<!doctype html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="utf-8" />
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
24
|
+
<title>{title ? `${title} — ${siteName}` : siteName}</title>
|
|
25
|
+
{description && <meta name="description" content={description} />}
|
|
26
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
27
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
28
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
29
|
+
<style is:global>
|
|
30
|
+
/* ==========================================================================
|
|
31
|
+
* The conference design: serif display, grotesk body, off-white ground,
|
|
32
|
+
* ink type, one editorial-red accent. Ported from the framework-live demo
|
|
33
|
+
* template; the only change is Söhne's removal (a commercial font) —
|
|
34
|
+
* Inter leads the sans stack instead — plus the four rules marked below.
|
|
35
|
+
* ========================================================================== */
|
|
36
|
+
|
|
37
|
+
:root {
|
|
38
|
+
--bg: #FAF7F2;
|
|
39
|
+
--bg-tint: #F2EDE4;
|
|
40
|
+
--ink: #14110F;
|
|
41
|
+
--ink-soft: #4A4540;
|
|
42
|
+
--ink-mute: #8A827A;
|
|
43
|
+
--rule: #D9D2C7;
|
|
44
|
+
--accent: #C8442B;
|
|
45
|
+
--accent-deep:#9E3320;
|
|
46
|
+
|
|
47
|
+
--serif: "Fraunces", "Source Serif 4", Georgia, "Times New Roman", serif;
|
|
48
|
+
--sans: "Inter", "Helvetica Neue", Arial, sans-serif;
|
|
49
|
+
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
50
|
+
|
|
51
|
+
--maxw: 1240px;
|
|
52
|
+
--gutter: clamp(20px, 4vw, 56px);
|
|
53
|
+
--rhythm: clamp(1.6rem, 0.4rem + 3vw, 2.4rem);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---- reset ------------------------------------------------------------- */
|
|
57
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
58
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
59
|
+
body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
background: var(--bg);
|
|
62
|
+
color: var(--ink);
|
|
63
|
+
font-family: var(--sans);
|
|
64
|
+
font-size: 16.5px;
|
|
65
|
+
line-height: 1.55;
|
|
66
|
+
-webkit-font-smoothing: antialiased;
|
|
67
|
+
text-rendering: optimizeLegibility;
|
|
68
|
+
}
|
|
69
|
+
img, svg { display: block; max-width: 100%; }
|
|
70
|
+
a { color: inherit; text-decoration: none; }
|
|
71
|
+
button { font: inherit; }
|
|
72
|
+
|
|
73
|
+
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
|
|
74
|
+
|
|
75
|
+
/* ---- type -------------------------------------------------------------- */
|
|
76
|
+
.display, .serif { font-family: var(--serif); font-weight: 400; letter-spacing: -0.01em; }
|
|
77
|
+
.eyebrow {
|
|
78
|
+
font-family: var(--sans);
|
|
79
|
+
font-size: 0.72rem;
|
|
80
|
+
text-transform: uppercase;
|
|
81
|
+
letter-spacing: 0.18em;
|
|
82
|
+
color: var(--accent);
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
h1, h2, h3, h4 { font-family: var(--serif); font-weight: 400; letter-spacing: -0.015em; line-height: 1.04; }
|
|
86
|
+
h1 { font-size: clamp(2.6rem, 6vw, 4.6rem); }
|
|
87
|
+
h2 { font-size: clamp(1.9rem, 3.6vw, 2.9rem); }
|
|
88
|
+
h3 { font-size: clamp(1.25rem, 1.6vw, 1.55rem); }
|
|
89
|
+
p { margin: 0 0 1em; color: var(--ink-soft); }
|
|
90
|
+
.lede { font-family: var(--serif); font-size: clamp(1.15rem, 1.8vw, 1.45rem); line-height: 1.4; color: var(--ink); }
|
|
91
|
+
|
|
92
|
+
/* ---- header ------------------------------------------------------------ */
|
|
93
|
+
.site-header { position: sticky; top: 0; z-index: 50; background: rgba(250,247,242,0.9); backdrop-filter: blur(10px); border-bottom: 1px solid var(--rule); }
|
|
94
|
+
.header-row { display: flex; align-items: baseline; gap: 1.5rem; padding: 1rem 0; }
|
|
95
|
+
.brand { display: flex; align-items: baseline; gap: 0.55rem; flex: 0 0 auto; }
|
|
96
|
+
.brand-mark { font-family: var(--serif); font-size: 1.3rem; font-weight: 500; letter-spacing: -0.02em; }
|
|
97
|
+
.brand-edition { font-family: var(--mono); font-size: 0.68rem; color: var(--ink-mute); letter-spacing: 0.05em; }
|
|
98
|
+
.site-nav { display: flex; gap: 1.6rem; margin-left: auto; }
|
|
99
|
+
.site-nav a { font-size: 0.92rem; color: var(--ink-soft); position: relative; padding-bottom: 2px; }
|
|
100
|
+
.site-nav a:hover { color: var(--ink); }
|
|
101
|
+
.site-nav a.is-active { color: var(--ink); }
|
|
102
|
+
.site-nav a.is-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -4px; height: 2px; background: var(--accent); }
|
|
103
|
+
.header-cta { font-size: 0.85rem; font-weight: 600; color: var(--bg); background: var(--ink); padding: 0.5rem 0.95rem; border-radius: 999px; }
|
|
104
|
+
.header-cta:hover { background: var(--accent); }
|
|
105
|
+
|
|
106
|
+
/* ---- footer ------------------------------------------------------------ */
|
|
107
|
+
.site-footer { margin-top: 6rem; border-top: 1px solid var(--rule); background: var(--bg-tint); }
|
|
108
|
+
.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1.4fr; gap: 2.5rem; padding-top: 3rem; padding-bottom: 2rem; }
|
|
109
|
+
.footer-brand .brand-mark { font-family: var(--serif); font-size: 1.5rem; }
|
|
110
|
+
.footer-brand p { font-family: var(--serif); font-style: italic; max-width: 26ch; }
|
|
111
|
+
.footer-col h4 { font-family: var(--sans); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.16em; color: var(--ink-mute); margin: 0 0 0.9rem; font-weight: 600; }
|
|
112
|
+
.footer-col a { display: block; font-size: 0.95rem; color: var(--ink-soft); padding: 0.18rem 0; }
|
|
113
|
+
.footer-col a:hover { color: var(--accent); }
|
|
114
|
+
.footer-meta p { font-size: 0.9rem; }
|
|
115
|
+
/* The bottom padding clears the fixed Studio button, which would otherwise sit on the credit. */
|
|
116
|
+
.footer-base { display: flex; justify-content: space-between; padding: 1.2rem 0 3.5rem; border-top: 1px solid var(--rule); font-size: 0.8rem; color: var(--ink-mute); }
|
|
117
|
+
|
|
118
|
+
/* ---- hero -------------------------------------------------------------- */
|
|
119
|
+
.hero { padding: clamp(3rem, 7vw, 6rem) 0 clamp(2.5rem, 5vw, 4rem); border-bottom: 1px solid var(--rule); }
|
|
120
|
+
.hero-eyebrow { margin-bottom: 1.2rem; }
|
|
121
|
+
.hero h1 { max-width: 16ch; margin: 0 0 1.4rem; }
|
|
122
|
+
.hero .lede { max-width: 52ch; margin-bottom: 2rem; }
|
|
123
|
+
.hero-meta { display: flex; flex-wrap: wrap; gap: 1.5rem 3rem; align-items: center; font-family: var(--mono); font-size: 0.82rem; color: var(--ink-soft); }
|
|
124
|
+
.hero-meta span { display: inline-flex; align-items: center; gap: 0.5rem; }
|
|
125
|
+
.hero-meta .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: inline-block; }
|
|
126
|
+
|
|
127
|
+
/* ---- stat strip -------------------------------------------------------- */
|
|
128
|
+
.stat-strip { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--rule); }
|
|
129
|
+
.stat { padding: 2rem 0; border-right: 1px solid var(--rule); }
|
|
130
|
+
.stat:last-child { border-right: none; }
|
|
131
|
+
.stat .v { font-family: var(--serif); font-size: clamp(2rem, 3.5vw, 3rem); }
|
|
132
|
+
.stat .l { font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-mute); margin-top: 0.3rem; }
|
|
133
|
+
|
|
134
|
+
/* ---- section frame ----------------------------------------------------- */
|
|
135
|
+
.section { padding: clamp(3rem, 6vw, 5rem) 0; border-bottom: 1px solid var(--rule); }
|
|
136
|
+
.section-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 2.5rem; gap: 2rem; flex-wrap: wrap; }
|
|
137
|
+
.section-head .eyebrow { display: block; margin-bottom: 0.6rem; }
|
|
138
|
+
.section-head a.more { font-family: var(--mono); font-size: 0.8rem; color: var(--accent); border-bottom: 1px solid var(--accent); padding-bottom: 1px; }
|
|
139
|
+
.section-head a.more:hover { color: var(--accent-deep); }
|
|
140
|
+
|
|
141
|
+
/* ---- speaker grid ------------------------------------------------------ */
|
|
142
|
+
.card-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(1.2rem, 2.5vw, 2.2rem); }
|
|
143
|
+
.card { display: block; border: 0; }
|
|
144
|
+
.avatar { display: block; border-radius: 2px; overflow: hidden; aspect-ratio: 1; line-height: 0; box-shadow: 0 1px 0 var(--rule); transition: transform 0.25s ease; }
|
|
145
|
+
.card:hover .avatar { transform: translateY(-4px); }
|
|
146
|
+
.card-body { padding-top: 0.9rem; }
|
|
147
|
+
.card-name { font-size: 1.15rem; margin: 0 0 0.2rem; }
|
|
148
|
+
.card-role { font-size: 0.86rem; color: var(--ink-mute); margin: 0; }
|
|
149
|
+
|
|
150
|
+
/* ---- session rows ------------------------------------------------------ */
|
|
151
|
+
.session-row { display: grid; grid-template-columns: 92px 84px 1fr 24px; gap: 1.2rem; align-items: baseline; padding: 1.15rem 0; border-top: 1px solid var(--rule); transition: background 0.18s ease; }
|
|
152
|
+
.session-row:hover { background: var(--bg-tint); }
|
|
153
|
+
.session-row:last-child { border-bottom: 1px solid var(--rule); }
|
|
154
|
+
.session-time { display: flex; flex-direction: column; font-family: var(--mono); font-size: 0.8rem; color: var(--ink); }
|
|
155
|
+
.session-time .t-end { color: var(--ink-mute); }
|
|
156
|
+
.session-pill { justify-self: start; font-family: var(--mono); font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.1em; color: #fff; background: var(--c); padding: 0.18rem 0.5rem; border-radius: 2px; align-self: center; }
|
|
157
|
+
.session-title { display: block; font-family: var(--serif); font-size: 1.2rem; color: var(--ink); margin-bottom: 0.25rem; }
|
|
158
|
+
.session-meta { font-size: 0.85rem; color: var(--ink-mute); }
|
|
159
|
+
.session-meta .session-speaker { color: var(--ink-soft); }
|
|
160
|
+
.session-meta .session-speaker + .session-speaker::before { content: " & "; }
|
|
161
|
+
.session-track { color: var(--ink-mute); }
|
|
162
|
+
.session-arrow { color: var(--ink-mute); transition: transform 0.18s ease, color 0.18s ease; }
|
|
163
|
+
.session-row:hover .session-arrow { color: var(--accent); transform: translateX(4px); }
|
|
164
|
+
|
|
165
|
+
/* ---- schedule grid ----------------------------------------------------- */
|
|
166
|
+
.schedule { display: grid; grid-template-columns: 140px 1fr; gap: 0; border-top: 1px solid var(--rule); }
|
|
167
|
+
.schedule-time { font-family: var(--mono); font-size: 0.8rem; color: var(--ink-mute); padding: 1.1rem 1rem 1.1rem 0; border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); text-align: right; }
|
|
168
|
+
.schedule-slots { display: grid; grid-template-columns: repeat(var(--tracks, 6), 1fr); border-bottom: 1px solid var(--rule); }
|
|
169
|
+
.schedule-slot { border-right: 1px solid var(--rule); padding: 0.9rem; min-height: 84px; }
|
|
170
|
+
.schedule-slot:last-child { border-right: none; }
|
|
171
|
+
.schedule-slot.is-empty { background: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--bg-tint) 6px, var(--bg-tint) 7px); }
|
|
172
|
+
.schedule-slot .slot-type { font-family: var(--mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--c); margin-bottom: 0.3rem; }
|
|
173
|
+
.schedule-slot .slot-title { font-family: var(--serif); font-size: 0.98rem; line-height: 1.2; margin-bottom: 0.35rem; }
|
|
174
|
+
.schedule-slot .slot-who { font-size: 0.76rem; color: var(--ink-mute); }
|
|
175
|
+
.schedule-slot a { color: inherit; }
|
|
176
|
+
.schedule-slot a:hover .slot-title { color: var(--accent); }
|
|
177
|
+
.schedule-slot .slot-bar { height: 3px; background: var(--c); width: 28px; margin-bottom: 0.5rem; }
|
|
178
|
+
|
|
179
|
+
.schedule-day-head { display: flex; justify-content: space-between; align-items: baseline; margin: 2.5rem 0 1rem; }
|
|
180
|
+
.schedule-day-head h2 { margin: 0; }
|
|
181
|
+
.schedule-day-head .date { font-family: var(--mono); font-size: 0.85rem; color: var(--ink-mute); }
|
|
182
|
+
.track-legend { display: flex; flex-wrap: wrap; gap: 0.9rem 1.6rem; margin: 0.4rem 0 1rem; }
|
|
183
|
+
.track-legend span { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.82rem; color: var(--ink-soft); }
|
|
184
|
+
.track-legend i { width: 10px; height: 10px; border-radius: 2px; }
|
|
185
|
+
.schedule-cols { display: grid; grid-template-columns: 140px repeat(var(--tracks, 6), 1fr); }
|
|
186
|
+
.schedule-cols .col-head { border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); padding: 0.7rem 0.9rem; }
|
|
187
|
+
.schedule-cols .col-head h4 { font-family: var(--sans); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.1em; margin: 0 0 0.15rem; font-weight: 600; }
|
|
188
|
+
.schedule-cols .col-head .room { font-size: 0.74rem; color: var(--ink-mute); }
|
|
189
|
+
.schedule-cols .col-spacer { border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }
|
|
190
|
+
|
|
191
|
+
/* ---- sponsors ---------------------------------------------------------- */
|
|
192
|
+
.sponsor-tier { margin-bottom: 2.5rem; }
|
|
193
|
+
.sponsor-tier-head { display: flex; align-items: baseline; gap: 1rem; margin-bottom: 1.2rem; }
|
|
194
|
+
.sponsor-tier-head h3 { margin: 0; text-transform: capitalize; }
|
|
195
|
+
.sponsor-tier-head .tier-line { flex: 1; height: 1px; background: var(--rule); }
|
|
196
|
+
.sponsor-grid { display: grid; gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
|
|
197
|
+
.sponsor-grid.tier-platinum { grid-template-columns: repeat(2, 1fr); }
|
|
198
|
+
.sponsor-grid.tier-gold { grid-template-columns: repeat(3, 1fr); }
|
|
199
|
+
.sponsor-grid.tier-silver { grid-template-columns: repeat(4, 1fr); }
|
|
200
|
+
.sponsor-grid.tier-partner { grid-template-columns: repeat(4, 1fr); }
|
|
201
|
+
.sponsor-card { background: var(--bg); padding: 1.8rem; display: flex; flex-direction: column; gap: 0.8rem; transition: background 0.18s ease; }
|
|
202
|
+
.sponsor-card:hover { background: var(--bg-tint); }
|
|
203
|
+
.sponsor-mark { font-family: var(--serif); font-size: 1.4rem; font-weight: 500; letter-spacing: -0.02em; padding: 0.3rem 0.7rem; color: #fff; background: var(--mark); align-self: flex-start; border-radius: 2px; }
|
|
204
|
+
.sponsor-blurb { font-size: 0.86rem; color: var(--ink-mute); margin: 0; }
|
|
205
|
+
.sponsor-card .sponsor-link { font-family: var(--mono); font-size: 0.74rem; color: var(--accent); margin-top: auto; }
|
|
206
|
+
|
|
207
|
+
/* ---- detail pages ------------------------------------------------------ */
|
|
208
|
+
.detail-head { padding: clamp(2.5rem, 5vw, 4rem) 0 2rem; border-bottom: 1px solid var(--rule); }
|
|
209
|
+
.back-link { font-family: var(--mono); font-size: 0.8rem; color: var(--ink-mute); display: inline-flex; gap: 0.4rem; margin-bottom: 1.5rem; }
|
|
210
|
+
.back-link:hover { color: var(--accent); }
|
|
211
|
+
.detail-grid { display: grid; grid-template-columns: 1fr 320px; gap: 3rem; align-items: start; }
|
|
212
|
+
.detail-main h1 { margin: 0 0 1.2rem; }
|
|
213
|
+
.detail-meta { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 2rem; }
|
|
214
|
+
.chip { font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.3rem 0.7rem; border: 1px solid var(--rule); border-radius: 2px; color: var(--ink-soft); }
|
|
215
|
+
.chip.accent { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
216
|
+
.detail-prose { font-size: 1.12rem; line-height: 1.6; max-width: 62ch; }
|
|
217
|
+
.detail-prose p { color: var(--ink); margin-bottom: 1.1em; }
|
|
218
|
+
.detail-aside { border-left: 1px solid var(--rule); padding-left: 2rem; }
|
|
219
|
+
.detail-aside h4 { font-family: var(--sans); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-mute); margin: 0 0 1rem; font-weight: 600; }
|
|
220
|
+
.kv { display: flex; justify-content: space-between; padding: 0.55rem 0; border-bottom: 1px solid var(--rule); font-size: 0.9rem; }
|
|
221
|
+
.kv .k { color: var(--ink-mute); font-family: var(--mono); font-size: 0.78rem; }
|
|
222
|
+
.kv .v { color: var(--ink); text-align: right; }
|
|
223
|
+
|
|
224
|
+
/* speaker detail */
|
|
225
|
+
.speaker-detail-grid { display: grid; grid-template-columns: 280px 1fr; gap: 3rem; align-items: start; }
|
|
226
|
+
.speaker-aside .avatar { width: 280px !important; height: 280px !important; }
|
|
227
|
+
.pronoun { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-mute); margin: 0.4rem 0 1.2rem; }
|
|
228
|
+
|
|
229
|
+
/* ---- venue ------------------------------------------------------------- */
|
|
230
|
+
.venue-hero { padding: clamp(2.5rem,5vw,4rem) 0; border-bottom: 1px solid var(--rule); }
|
|
231
|
+
.venue-map { aspect-ratio: 16/7; background: var(--bg-tint); border: 1px solid var(--rule); position: relative; overflow: hidden; margin-top: 2rem; }
|
|
232
|
+
.venue-map svg { width: 100%; height: 100%; }
|
|
233
|
+
.travel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; border-top: 1px solid var(--rule); }
|
|
234
|
+
.travel-item { padding: 1.8rem 0; border-bottom: 1px solid var(--rule); border-right: 1px solid var(--rule); padding-right: 2rem; }
|
|
235
|
+
.travel-item:nth-child(2n) { border-right: none; }
|
|
236
|
+
.travel-item h3 { font-size: 1.3rem; margin: 0 0 0.5rem; }
|
|
237
|
+
.travel-item .mode { font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--accent); margin-bottom: 0.5rem; }
|
|
238
|
+
|
|
239
|
+
/* tickets */
|
|
240
|
+
.tickets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
|
|
241
|
+
.ticket { border: 1px solid var(--rule); padding: 2rem; display: flex; flex-direction: column; background: var(--bg); }
|
|
242
|
+
.ticket.is-featured { border-color: var(--ink); background: var(--ink); color: var(--bg); }
|
|
243
|
+
.ticket .t-name { font-family: var(--mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.14em; }
|
|
244
|
+
.ticket .t-price { font-family: var(--serif); font-size: 2.6rem; margin: 0.5rem 0 0.3rem; }
|
|
245
|
+
.ticket .t-note { font-size: 0.86rem; color: var(--ink-mute); margin: 0 0 1.4rem; }
|
|
246
|
+
.ticket.is-featured .t-note { color: rgba(250,247,242,0.7); }
|
|
247
|
+
.ticket ul { list-style: none; margin: 0 0 1.6rem; padding: 0; }
|
|
248
|
+
.ticket li { font-size: 0.92rem; padding: 0.35rem 0; display: flex; gap: 0.6rem; }
|
|
249
|
+
.ticket li::before { content: "+"; color: var(--accent); font-family: var(--mono); }
|
|
250
|
+
.ticket.is-featured li::before { color: var(--bg); }
|
|
251
|
+
.ticket .t-cta { margin-top: auto; font-family: var(--mono); font-size: 0.82rem; padding: 0.7rem; text-align: center; border: 1px solid currentColor; }
|
|
252
|
+
.ticket.is-featured .t-cta { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
253
|
+
|
|
254
|
+
/* ---- generic helpers --------------------------------------------------- */
|
|
255
|
+
.tag-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
256
|
+
.tag { font-family: var(--mono); font-size: 0.72rem; color: var(--ink-soft); background: var(--bg-tint); padding: 0.25rem 0.6rem; border-radius: 2px; }
|
|
257
|
+
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 1.5rem; }
|
|
258
|
+
.filter-bar button, .filter-bar a { font-family: var(--mono); font-size: 0.8rem; padding: 0.4rem 0.9rem; border: 1px solid var(--rule); background: var(--bg); color: var(--ink-soft); border-radius: 999px; cursor: pointer; }
|
|
259
|
+
.filter-bar button:hover, .filter-bar a:hover { border-color: var(--ink); color: var(--ink); }
|
|
260
|
+
.filter-bar button.is-active, .filter-bar a.is-active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
261
|
+
.empty-note { padding: 2rem 0; color: var(--ink-mute); font-style: italic; }
|
|
262
|
+
/* The empty notes link to Studio; muted italic alone gave an invisible link nothing to click on. */
|
|
263
|
+
.empty-note a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
|
264
|
+
.section-noborder { border-bottom: none; }
|
|
265
|
+
|
|
266
|
+
/* ---- port additions -------------------------------------------------- */
|
|
267
|
+
img.avatar { object-fit: cover; }
|
|
268
|
+
.sponsor-mark-img { align-self: flex-start; max-height: 84px; width: auto; }
|
|
269
|
+
.session-day-title { font-family: var(--mono); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-mute); margin: 2.2rem 0 0.6rem; }
|
|
270
|
+
.studio { position: fixed; right: 16px; bottom: 16px; font: 13px "Inter", system-ui, sans-serif; background: var(--accent); color: #fff; padding: 8px 12px; border-radius: 999px; opacity: .9 }
|
|
271
|
+
|
|
272
|
+
/* ---- responsive -------------------------------------------------------- */
|
|
273
|
+
@media (max-width: 1000px) {
|
|
274
|
+
.card-grid { grid-template-columns: repeat(3, 1fr); }
|
|
275
|
+
.stat-strip { grid-template-columns: repeat(2, 1fr); }
|
|
276
|
+
.stat:nth-child(2) { border-right: none; }
|
|
277
|
+
.stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--rule); }
|
|
278
|
+
.footer-grid { grid-template-columns: 1fr 1fr; }
|
|
279
|
+
.detail-grid, .speaker-detail-grid { grid-template-columns: 1fr; gap: 2rem; }
|
|
280
|
+
.detail-aside, .speaker-aside { border-left: none; border-top: 1px solid var(--rule); padding-left: 0; padding-top: 1.5rem; }
|
|
281
|
+
.schedule, .schedule-cols { grid-template-columns: 70px repeat(var(--tracks,6), 1fr); font-size: 0.8rem; }
|
|
282
|
+
.schedule-slot { padding: 0.5rem 0.4rem; min-height: 70px; }
|
|
283
|
+
.schedule-slot .slot-title { font-size: 0.82rem; }
|
|
284
|
+
}
|
|
285
|
+
@media (max-width: 700px) {
|
|
286
|
+
.site-nav { display: none; }
|
|
287
|
+
.header-cta { margin-left: auto; }
|
|
288
|
+
.card-grid { grid-template-columns: repeat(2, 1fr); }
|
|
289
|
+
.footer-grid { grid-template-columns: 1fr; }
|
|
290
|
+
.tickets { grid-template-columns: 1fr; }
|
|
291
|
+
.travel-grid { grid-template-columns: 1fr; }
|
|
292
|
+
.travel-item { border-right: none; padding-right: 0; }
|
|
293
|
+
.session-row { grid-template-columns: 70px 1fr; row-gap: 0.4rem; }
|
|
294
|
+
.session-pill, .session-arrow { grid-column: 2; }
|
|
295
|
+
.schedule, .schedule-cols { grid-template-columns: 50px repeat(var(--tracks,6), minmax(44px, 1fr)); }
|
|
296
|
+
.schedule-slot .slot-who { display: none; }
|
|
297
|
+
.schedule-slot .slot-title { font-size: 0.74rem; }
|
|
298
|
+
.col-head .room, .schedule-time { font-size: 0.62rem; }
|
|
299
|
+
.sponsor-grid.tier-platinum, .sponsor-grid.tier-gold,
|
|
300
|
+
.sponsor-grid.tier-silver, .sponsor-grid.tier-partner { grid-template-columns: 1fr 1fr; }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ---- print niceties ---------------------------------------------------- */
|
|
304
|
+
@media (prefers-reduced-motion: reduce) {
|
|
305
|
+
* { transition: none !important; animation: none !important; }
|
|
306
|
+
}
|
|
307
|
+
</style>
|
|
308
|
+
</head>
|
|
309
|
+
<body>
|
|
310
|
+
<header class="site-header"><div class="wrap header-row">
|
|
311
|
+
<a class="brand" href="/">
|
|
312
|
+
<span class="brand-mark">{siteName}</span>
|
|
313
|
+
{ev?.edition && <span class="brand-edition">{ev.edition}</span>}
|
|
314
|
+
</a>
|
|
315
|
+
<nav class="site-nav">
|
|
316
|
+
{nav.map(item => <a href={item.href} class={current === item.href || (item.href !== '/' && current.startsWith(`${item.href}/`)) ? 'is-active' : ''}>{item.label}</a>)}
|
|
317
|
+
</nav>
|
|
318
|
+
{ev?.ctaLabel && ev?.ctaHref && <a class="header-cta" href={ev.ctaHref}>{ev.ctaLabel}</a>}
|
|
319
|
+
</div></header>
|
|
320
|
+
|
|
321
|
+
<main><slot /></main>
|
|
322
|
+
|
|
323
|
+
{ev && (
|
|
324
|
+
<footer class="site-footer">
|
|
325
|
+
<div class="wrap footer-grid">
|
|
326
|
+
<div class="footer-brand">
|
|
327
|
+
<span class="brand-mark">{ev.name}</span>
|
|
328
|
+
{ev.tagline && <p>{ev.tagline}</p>}
|
|
329
|
+
</div>
|
|
330
|
+
<div class="footer-col">
|
|
331
|
+
<h4>Programme</h4>
|
|
332
|
+
<a href="/schedule">Schedule</a>
|
|
333
|
+
<a href="/speakers">Speakers</a>
|
|
334
|
+
<a href="/sessions">Sessions</a>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="footer-col">
|
|
337
|
+
<h4>Visit</h4>
|
|
338
|
+
<a href="/venue">Venue & travel</a>
|
|
339
|
+
<a href="/sponsors">Sponsors</a>
|
|
340
|
+
{ev.ctaHref && <a href={ev.ctaHref}>Get a pass</a>}
|
|
341
|
+
</div>
|
|
342
|
+
<div class="footer-col footer-meta">
|
|
343
|
+
<h4>{ev.city}</h4>
|
|
344
|
+
{ev.venue && <p>{ev.venue}</p>}
|
|
345
|
+
{(ev.startDate || ev.endDate) && <p>{formatDateRange(ev.startDate, ev.endDate)}</p>}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="wrap footer-base">
|
|
349
|
+
<span>© {[ev.edition, ev.name].filter(Boolean).join(' ')}. Every word here is editable in Studio.</span>
|
|
350
|
+
<span class="footer-credit">A Trokky template</span>
|
|
351
|
+
</div>
|
|
352
|
+
</footer>
|
|
353
|
+
)}
|
|
354
|
+
<a class="studio" href="/studio">Edit in Studio →</a>
|
|
355
|
+
</body>
|
|
356
|
+
</html>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../layouts/Base.astro'
|
|
3
|
+
import { load } from '../trokky/page'
|
|
4
|
+
import { formatDateRange, formatDate } from '../trokky/site'
|
|
5
|
+
import SpeakerCard from '../components/SpeakerCard.astro'
|
|
6
|
+
import SessionCard from '../components/SessionCard.astro'
|
|
7
|
+
import SponsorMark from '../components/SponsorMark.astro'
|
|
8
|
+
const s = await load(Astro)
|
|
9
|
+
const ev = s.event
|
|
10
|
+
const featured = (await s.speakers()).slice(0, 4)
|
|
11
|
+
const days = await s.days()
|
|
12
|
+
const dayOne = (await (s.sessions({ day: days[0] }))).slice(0, 5)
|
|
13
|
+
const firstDay = dayOne[0]?.day ?? days[0]
|
|
14
|
+
const platinum = (await s.sponsorsByTier())[0]?.sponsors ?? []
|
|
15
|
+
const stats = ev?.stats ?? []
|
|
16
|
+
const speakerCount = ev?.stats?.[3]?.value ?? String(featured.length)
|
|
17
|
+
---
|
|
18
|
+
<Base event={ev} description={ev?.tagline}>
|
|
19
|
+
{!ev && (
|
|
20
|
+
<section class="hero"><div class="wrap">
|
|
21
|
+
<p class="empty-note lede">Nothing here yet — open <a href="/studio">Studio</a>, claim the instance, and the conference seeds itself.</p>
|
|
22
|
+
</div></section>
|
|
23
|
+
)}
|
|
24
|
+
{ev && (
|
|
25
|
+
<section class="hero"><div class="wrap">
|
|
26
|
+
<div class="hero-eyebrow eyebrow">{ev.edition} edition · {ev.city}</div>
|
|
27
|
+
<h1 class="display">{ev.name}</h1>
|
|
28
|
+
<p class="lede">{ev.tagline}</p>
|
|
29
|
+
{ev.description && <p style="max-width:54ch;margin-bottom:2rem">{ev.description}</p>}
|
|
30
|
+
<div class="hero-meta">
|
|
31
|
+
<span><i class="dot"></i>{formatDateRange(ev.startDate, ev.endDate)}</span>
|
|
32
|
+
{ev.venue && <span>{ev.venue}, {ev.city}</span>}
|
|
33
|
+
{stats[2] && <span>{stats[2].value} sessions</span>}
|
|
34
|
+
</div>
|
|
35
|
+
</div></section>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{stats.length > 0 && (
|
|
39
|
+
<section><div class="wrap"><div class="stat-strip">
|
|
40
|
+
{stats.map((st: any) => <div class="stat"><div class="v">{st.value}</div><div class="l">{st.label}</div></div>)}
|
|
41
|
+
</div></div></section>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
{featured.length > 0 && (
|
|
45
|
+
<section class="section"><div class="wrap">
|
|
46
|
+
<div class="section-head">
|
|
47
|
+
<div>
|
|
48
|
+
<span class="eyebrow">Featured speakers</span>
|
|
49
|
+
<h2 class="display">The people on the main stage</h2>
|
|
50
|
+
</div>
|
|
51
|
+
<a class="more" href="/speakers">All {speakerCount} →</a>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="card-grid">{featured.map(sp => <SpeakerCard speaker={sp} />)}</div>
|
|
54
|
+
</div></section>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{dayOne.length > 0 && (
|
|
58
|
+
<section class="section section-noborder"><div class="wrap">
|
|
59
|
+
<div class="section-head">
|
|
60
|
+
<div>
|
|
61
|
+
<span class="eyebrow">Day one · {formatDate(firstDay)}</span>
|
|
62
|
+
<h2 class="display">The opening line-up</h2>
|
|
63
|
+
</div>
|
|
64
|
+
<a class="more" href="/schedule">Full schedule →</a>
|
|
65
|
+
</div>
|
|
66
|
+
{dayOne.map(x => <SessionCard session={x} />)}
|
|
67
|
+
</div></section>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{platinum.length > 0 && (
|
|
71
|
+
<section class="section"><div class="wrap">
|
|
72
|
+
<div class="section-head">
|
|
73
|
+
<div>
|
|
74
|
+
<span class="eyebrow">With thanks to</span>
|
|
75
|
+
<h2 class="display">Our platinum partners</h2>
|
|
76
|
+
</div>
|
|
77
|
+
<a class="more" href="/sponsors">All sponsors →</a>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="sponsor-grid tier-platinum">{platinum.map(sp => <SponsorMark sponsor={sp} />)}</div>
|
|
80
|
+
</div></section>
|
|
81
|
+
)}
|
|
82
|
+
</Base>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../layouts/Base.astro'
|
|
3
|
+
import { load } from '../trokky/page'
|
|
4
|
+
import { formatDate } from '../trokky/site'
|
|
5
|
+
const s = await load(Astro)
|
|
6
|
+
const tracks = await s.tracks()
|
|
7
|
+
const days = await s.days()
|
|
8
|
+
const dayBlocks = await Promise.all(days.map(async day => ({ day, sessions: await s.sessions({ day }) })))
|
|
9
|
+
---
|
|
10
|
+
<Base title="Schedule" event={s.event}>
|
|
11
|
+
<section class="detail-head"><div class="wrap">
|
|
12
|
+
<span class="eyebrow">{days.length} days · {tracks.length} tracks</span>
|
|
13
|
+
<h1 class="display">The full programme</h1>
|
|
14
|
+
<p class="lede" style="max-width:54ch;color:var(--ink-soft)">Every session, every room. Click a block for the talk detail.</p>
|
|
15
|
+
</div></section>
|
|
16
|
+
|
|
17
|
+
<section class="section section-noborder"><div class="wrap">
|
|
18
|
+
{dayBlocks.map(({ day, sessions }, i) => {
|
|
19
|
+
const rows = [...new Set(sessions.map(x => String(x.start)))].sort((a, b) => (a < b ? -1 : 1))
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<div class="schedule-day-head">
|
|
23
|
+
<h2 class="display">{formatDate(day)}</h2>
|
|
24
|
+
<span class="date">Day {i + 1} · {sessions.length} sessions</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="track-legend">
|
|
27
|
+
{tracks.map(t => <span><i style={`background:${t.color || '#333'}`}></i>{t.name}</span>)}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="schedule-cols" style={`--tracks: ${tracks.length}`}>
|
|
30
|
+
<div class="col-spacer"></div>
|
|
31
|
+
{tracks.map(t => <div class="col-head"><h4>{t.name}</h4><div class="room">{t.room}</div></div>)}
|
|
32
|
+
</div>
|
|
33
|
+
<div class="schedule" style={`--tracks: ${tracks.length}`}>
|
|
34
|
+
{rows.map(time => {
|
|
35
|
+
const slot = sessions.filter(x => String(x.start) === time)
|
|
36
|
+
const endTime = String(slot[0]?.end ?? '')
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<div class="schedule-time">{time}<br /><span style="color:var(--ink-mute)">{endTime}</span></div>
|
|
40
|
+
<div class="schedule-slots" style={`--tracks: ${tracks.length}`}>
|
|
41
|
+
{tracks.map(t => {
|
|
42
|
+
const x = slot.find(y => y.track?.id === t.id)
|
|
43
|
+
if (!x) return <div class="schedule-slot is-empty"></div>
|
|
44
|
+
return (
|
|
45
|
+
<div class="schedule-slot" style={`--c:${t.color || '#333'}`}>
|
|
46
|
+
<div class="slot-bar"></div>
|
|
47
|
+
<a href={`/sessions/${x.id}`}>
|
|
48
|
+
<div class="slot-type">{x.type}</div>
|
|
49
|
+
<div class="slot-title">{x.title}</div>
|
|
50
|
+
<div class="slot-who">{x.speakers.map((sp: any) => sp.name).join(' · ')}</div>
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
{!dayBlocks.length && <p class="empty-note">No sessions yet — open <a href="/studio">Studio</a> and add the first one.</p>}
|
|
64
|
+
</div></section>
|
|
65
|
+
</Base>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../../layouts/Base.astro'
|
|
3
|
+
import { load } from '../../trokky/page'
|
|
4
|
+
import { formatDate } from '../../trokky/site'
|
|
5
|
+
import Avatar from '../../components/Avatar.astro'
|
|
6
|
+
const s = await load(Astro)
|
|
7
|
+
const x = await s.session(Astro.params.id!)
|
|
8
|
+
if (!x) return new Response(null, { status: 404 })
|
|
9
|
+
---
|
|
10
|
+
<Base title={x.title} event={s.event}>
|
|
11
|
+
<section><div class="wrap">
|
|
12
|
+
<a class="back-link" href="/sessions">← All sessions</a>
|
|
13
|
+
<div class="detail-grid">
|
|
14
|
+
<div class="detail-main">
|
|
15
|
+
<div class="detail-meta">
|
|
16
|
+
<span class="chip accent">{x.type}</span>
|
|
17
|
+
{x.track && <span class="chip">{x.track.name}</span>}
|
|
18
|
+
<span class="chip">{formatDate(x.day)}</span>
|
|
19
|
+
<span class="chip">{x.start}–{x.end}</span>
|
|
20
|
+
{x.level && <span class="chip">{x.level}</span>}
|
|
21
|
+
</div>
|
|
22
|
+
<h1 class="display">{x.title}</h1>
|
|
23
|
+
{x.abstract && <p class="detail-prose">{x.abstract}</p>}
|
|
24
|
+
{(x.tags ?? []).length > 0 && (
|
|
25
|
+
<div class="tag-row" style="margin-top:1.5rem">
|
|
26
|
+
{x.tags.map((t: string) => <span class="tag">#{t}</span>)}
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
{x.speakers.length > 0 && (
|
|
30
|
+
<>
|
|
31
|
+
<h3 class="serif" style="margin:2.5rem 0 1rem">Featuring</h3>
|
|
32
|
+
<div class="card-grid" style="grid-template-columns:repeat(2,1fr);max-width:480px">
|
|
33
|
+
{x.speakers.map(sp => (
|
|
34
|
+
<a class="card speaker-card" href={`/speakers/${sp.id}`}>
|
|
35
|
+
<Avatar speaker={sp} size={160} />
|
|
36
|
+
<div class="card-body">
|
|
37
|
+
<h3 class="card-name">{sp.name}</h3>
|
|
38
|
+
<p class="card-role">{sp.role}</p>
|
|
39
|
+
</div>
|
|
40
|
+
</a>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<aside class="detail-aside">
|
|
47
|
+
<h4>Where & when</h4>
|
|
48
|
+
<div class="kv"><span class="k">Room</span><span class="v">{x.track?.room || '—'}</span></div>
|
|
49
|
+
<div class="kv"><span class="k">Track</span><span class="v">{x.track?.name || '—'}</span></div>
|
|
50
|
+
<div class="kv"><span class="k">Day</span><span class="v">{formatDate(x.day)}</span></div>
|
|
51
|
+
<div class="kv"><span class="k">Time</span><span class="v">{x.start}–{x.end}</span></div>
|
|
52
|
+
<div class="kv"><span class="k">Level</span><span class="v">{x.level}</span></div>
|
|
53
|
+
<div class="kv" style="border:none"><span class="k">Type</span><span class="v">{x.type}</span></div>
|
|
54
|
+
</aside>
|
|
55
|
+
</div>
|
|
56
|
+
</div></section>
|
|
57
|
+
</Base>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../../layouts/Base.astro'
|
|
3
|
+
import { load } from '../../trokky/page'
|
|
4
|
+
import { formatDate } from '../../trokky/site'
|
|
5
|
+
import SessionCard from '../../components/SessionCard.astro'
|
|
6
|
+
const s = await load(Astro)
|
|
7
|
+
const tracks = await s.tracks()
|
|
8
|
+
const all = await s.sessions()
|
|
9
|
+
const types = [...new Set(all.map(x => String(x.type)))]
|
|
10
|
+
const type = Astro.url.searchParams.get('type') ?? ''
|
|
11
|
+
const track = Astro.url.searchParams.get('track') ?? ''
|
|
12
|
+
const list = await s.sessions({ type: type || undefined, track: track || undefined })
|
|
13
|
+
const groups: { day: string; items: Record<string, any>[] }[] = []
|
|
14
|
+
// A session with no parseable day has no group header to sit under; it renders on its own detail
|
|
15
|
+
// page and nowhere else.
|
|
16
|
+
for (const x of list.filter((x: { day?: string }) => x.day)) {
|
|
17
|
+
const g = groups[groups.length - 1]
|
|
18
|
+
if (g && g.day === String(x.day)) g.items.push(x)
|
|
19
|
+
else groups.push({ day: String(x.day), items: [x] })
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Filter-bar link: carries the current search params except the one being changed (empty string clears).
|
|
23
|
+
*/
|
|
24
|
+
const link = (next: { type?: string; track?: string }) => {
|
|
25
|
+
const t = next.type !== undefined ? next.type : type
|
|
26
|
+
const k = next.track !== undefined ? next.track : track
|
|
27
|
+
const q = new URLSearchParams()
|
|
28
|
+
if (t) q.set('type', t)
|
|
29
|
+
if (k) q.set('track', k)
|
|
30
|
+
return q.toString() ? `/sessions?${q.toString()}` : '/sessions'
|
|
31
|
+
}
|
|
32
|
+
---
|
|
33
|
+
<Base title="Sessions" event={s.event}>
|
|
34
|
+
<section class="detail-head"><div class="wrap">
|
|
35
|
+
<span class="eyebrow">{list.length} sessions</span>
|
|
36
|
+
<h1 class="display">Every talk, workshop & panel</h1>
|
|
37
|
+
<p class="lede" style="max-width:54ch;color:var(--ink-soft)">Filter by type or track — every session on the programme, in day order.</p>
|
|
38
|
+
</div></section>
|
|
39
|
+
|
|
40
|
+
<section class="section section-noborder"><div class="wrap">
|
|
41
|
+
{all.length > 0 && (
|
|
42
|
+
<>
|
|
43
|
+
<div class="filter-bar">
|
|
44
|
+
<a href={link({ type: '' })} class={!type ? 'is-active' : ''}>All</a>
|
|
45
|
+
{types.map(t => <a href={link({ type: t })} class={type === t ? 'is-active' : ''}>{t}</a>)}
|
|
46
|
+
</div>
|
|
47
|
+
<div class="filter-bar">
|
|
48
|
+
<a href={link({ track: '' })} class={!track ? 'is-active' : ''}>Every track</a>
|
|
49
|
+
{tracks.map(t => <a href={link({ track: String(t.id) })} class={track === String(t.id) ? 'is-active' : ''}>{t.name}</a>)}
|
|
50
|
+
</div>
|
|
51
|
+
{groups.map(g => (
|
|
52
|
+
<>
|
|
53
|
+
<h3 class="session-day-title">{formatDate(g.day)}</h3>
|
|
54
|
+
{g.items.map(x => <SessionCard session={x} />)}
|
|
55
|
+
</>
|
|
56
|
+
))}
|
|
57
|
+
{!list.length && <p class="empty-note">No sessions match those filters.</p>}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
{!all.length && <p class="empty-note">No sessions yet — open <a href="/studio">Studio</a> and add the first one.</p>}
|
|
61
|
+
</div></section>
|
|
62
|
+
</Base>
|