create-trokky 0.1.2 → 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/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 +23 -2
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../../layouts/Base.astro'
|
|
3
|
+
import { load } from '../../trokky/page'
|
|
4
|
+
import Avatar from '../../components/Avatar.astro'
|
|
5
|
+
import SessionCard from '../../components/SessionCard.astro'
|
|
6
|
+
const s = await load(Astro)
|
|
7
|
+
const sp = await s.speaker(Astro.params.id!)
|
|
8
|
+
if (!sp) return new Response(null, { status: 404 })
|
|
9
|
+
---
|
|
10
|
+
<Base title={sp.name} event={s.event}>
|
|
11
|
+
<section><div class="wrap">
|
|
12
|
+
<a class="back-link" href="/speakers">← All speakers</a>
|
|
13
|
+
<div class="speaker-detail-grid">
|
|
14
|
+
<div class="speaker-aside">
|
|
15
|
+
<Avatar speaker={sp} size={280} />
|
|
16
|
+
{(sp.pronouns || sp.city) && <p class="pronoun">{[sp.pronouns, sp.city].filter(Boolean).join(" · ")}</p>}
|
|
17
|
+
</div>
|
|
18
|
+
<div class="detail-main">
|
|
19
|
+
{sp.role && <div class="eyebrow">{sp.role}</div>}
|
|
20
|
+
<h1 class="display">{sp.name}</h1>
|
|
21
|
+
{sp.org && <p class="lede" style="color:var(--ink-soft)">{sp.org}</p>}
|
|
22
|
+
{sp.bio && <p class="detail-prose">{sp.bio}</p>}
|
|
23
|
+
{sp.sessions.length > 0 && (
|
|
24
|
+
<>
|
|
25
|
+
<h3 class="serif" style="margin:2.5rem 0 1rem">Sessions at {s.event?.name}</h3>
|
|
26
|
+
{sp.sessions.map(x => <SessionCard session={x} showDay />)}
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div></section>
|
|
32
|
+
</Base>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../../layouts/Base.astro'
|
|
3
|
+
import { load } from '../../trokky/page'
|
|
4
|
+
import SpeakerCard from '../../components/SpeakerCard.astro'
|
|
5
|
+
const s = await load(Astro)
|
|
6
|
+
const speakers = await s.speakers()
|
|
7
|
+
---
|
|
8
|
+
<Base title="Speakers" event={s.event}>
|
|
9
|
+
<section class="detail-head"><div class="wrap">
|
|
10
|
+
<span class="eyebrow">{speakers.length} speakers</span>
|
|
11
|
+
<h1 class="display">The line-up</h1>
|
|
12
|
+
<p class="lede" style="max-width:54ch;color:var(--ink-soft)">The people on the stages and in the rooms — every card links to the speaker's own page, with their sessions.</p>
|
|
13
|
+
</div></section>
|
|
14
|
+
|
|
15
|
+
<section class="section section-noborder"><div class="wrap">
|
|
16
|
+
{speakers.length > 0
|
|
17
|
+
? <div class="card-grid">{speakers.map(sp => <SpeakerCard speaker={sp} />)}</div>
|
|
18
|
+
: <p class="empty-note">No speakers yet — open <a href="/studio">Studio</a> and add the first one.</p>}
|
|
19
|
+
</div></section>
|
|
20
|
+
</Base>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../layouts/Base.astro'
|
|
3
|
+
import { load } from '../trokky/page'
|
|
4
|
+
import SponsorMark from '../components/SponsorMark.astro'
|
|
5
|
+
const s = await load(Astro)
|
|
6
|
+
const tiers = await s.sponsorsByTier()
|
|
7
|
+
---
|
|
8
|
+
<Base title="Sponsors" event={s.event}>
|
|
9
|
+
<section class="detail-head"><div class="wrap">
|
|
10
|
+
<span class="eyebrow">Grouped by tier</span>
|
|
11
|
+
<h1 class="display">Sponsors</h1>
|
|
12
|
+
<p class="lede" style="max-width:54ch;color:var(--ink-soft)">The partners who make Frameworks Live possible, grouped by tier.</p>
|
|
13
|
+
</div></section>
|
|
14
|
+
|
|
15
|
+
<section class="section section-noborder"><div class="wrap">
|
|
16
|
+
{tiers.length > 0 ? tiers.map(t => (
|
|
17
|
+
<div class="sponsor-tier">
|
|
18
|
+
<div class="sponsor-tier-head">
|
|
19
|
+
<h3 class="serif">{t.tier}</h3>
|
|
20
|
+
<span class="tier-line"></span>
|
|
21
|
+
<span style="font-family:var(--mono);font-size:0.78rem;color:var(--ink-mute)">{t.sponsors.length} sponsor{t.sponsors.length > 1 ? 's' : ''}</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div class={`sponsor-grid tier-${t.tier}`}>
|
|
24
|
+
{t.sponsors.map(sp => (
|
|
25
|
+
<a class="sponsor-card" href={sp.url || '#'}>
|
|
26
|
+
<SponsorMark sponsor={sp} />
|
|
27
|
+
<p class="sponsor-blurb">{sp.blurb}</p>
|
|
28
|
+
<span class="sponsor-link">visit {String(sp.name).toLowerCase()} →</span>
|
|
29
|
+
</a>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)) : (
|
|
34
|
+
<p class="empty-note">No sponsors yet — open <a href="/studio">Studio</a> and add the first one.</p>
|
|
35
|
+
)}
|
|
36
|
+
</div></section>
|
|
37
|
+
</Base>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Base from '../layouts/Base.astro'
|
|
3
|
+
import { load } from '../trokky/page'
|
|
4
|
+
const s = await load(Astro)
|
|
5
|
+
const v = s.venue
|
|
6
|
+
const ev = s.event
|
|
7
|
+
const firstWord = v?.name ? String(v.name).split(' ')[0] : 'Hall'
|
|
8
|
+
const verticals = Array.from({ length: 9 }, (_, i) => i * 100)
|
|
9
|
+
const horizontals = Array.from({ length: 4 }, (_, i) => i * 90)
|
|
10
|
+
---
|
|
11
|
+
<Base title="Venue & tickets" event={ev}>
|
|
12
|
+
<section class="venue-hero"><div class="wrap">
|
|
13
|
+
{v ? (
|
|
14
|
+
<>
|
|
15
|
+
<div class="eyebrow">The hall</div>
|
|
16
|
+
<h1 class="display" style="margin:0.4rem 0 1.2rem">{v.name}</h1>
|
|
17
|
+
<p class="lede">{v.address}</p>
|
|
18
|
+
{v.about && <p style="max-width:62ch">{v.about}</p>}
|
|
19
|
+
</>
|
|
20
|
+
) : (
|
|
21
|
+
<p class="empty-note lede">No venue yet — open <a href="/studio">Studio</a> and describe the hall.</p>
|
|
22
|
+
)}
|
|
23
|
+
<div class="venue-map">
|
|
24
|
+
<svg viewBox="0 0 800 360" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
|
|
25
|
+
<rect width="800" height="360" fill="#F2EDE4" />
|
|
26
|
+
{verticals.map(x => <line x1={x} y1={0} x2={x} y2={360} stroke="#E4DCCD" stroke-width={1} />)}
|
|
27
|
+
{horizontals.map(y => <line x1={0} y1={y} x2={800} y2={y} stroke="#E4DCCD" stroke-width={1} />)}
|
|
28
|
+
<rect x={60} y={120} width={160} height={120} fill="none" stroke="#14110F" stroke-width={2} />
|
|
29
|
+
<text x={140} y={185} text-anchor="middle" font-family="Fraunces, serif" font-size={22} fill="#14110F">{firstWord}</text>
|
|
30
|
+
<path d="M250 220 Q 400 230 540 250 L 800 320" stroke="#3D6B9E" stroke-width={14} fill="none" opacity={0.5} />
|
|
31
|
+
<circle cx={140} cy={180} r={14} fill="#C8442B" />
|
|
32
|
+
<circle cx={140} cy={180} r={28} fill="#C8442B" opacity={0.2} />
|
|
33
|
+
<text x={170} y={186} font-family="JetBrains Mono, monospace" font-size={13} fill="#4A4540">{v?.name ?? ''}</text>
|
|
34
|
+
<text x={470} y={300} font-family="JetBrains Mono, monospace" font-size={12} fill="#3D6B9E">Rio Tejo</text>
|
|
35
|
+
</svg>
|
|
36
|
+
</div>
|
|
37
|
+
</div></section>
|
|
38
|
+
|
|
39
|
+
<section class="section"><div class="wrap">
|
|
40
|
+
<div class="section-head">
|
|
41
|
+
<div><span class="eyebrow">Getting there</span><h2 class="display">Travel</h2></div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="travel-grid">
|
|
44
|
+
{(v?.travel ?? []).length > 0
|
|
45
|
+
? v.travel.map((t: any) => (
|
|
46
|
+
<div class="travel-item">
|
|
47
|
+
<div class="mode">{t.mode}</div>
|
|
48
|
+
<p>{t.detail}</p>
|
|
49
|
+
</div>
|
|
50
|
+
))
|
|
51
|
+
: <p class="empty-note">No travel details yet — add them to the Venue document in Studio.</p>}
|
|
52
|
+
</div>
|
|
53
|
+
</div></section>
|
|
54
|
+
|
|
55
|
+
<section class="section section-noborder" id="tickets"><div class="wrap">
|
|
56
|
+
<div class="section-head">
|
|
57
|
+
<div><span class="eyebrow">Tickets</span><h2 class="display">Get a pass</h2></div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="tickets">
|
|
60
|
+
{(v?.tickets ?? []).length > 0
|
|
61
|
+
? v.tickets.map((t: any) => (
|
|
62
|
+
<div class={`ticket ${t.featured ? 'is-featured' : ''}`}>
|
|
63
|
+
<div class="t-name">{t.name}</div>
|
|
64
|
+
<div class="t-price">{t.price}</div>
|
|
65
|
+
<p class="t-note">{t.note}</p>
|
|
66
|
+
<ul>{(t.features ?? []).map((f: string) => <li>{f}</li>)}</ul>
|
|
67
|
+
<a class="t-cta" href="#">Reserve a {String(t.name).toLowerCase()} pass</a>
|
|
68
|
+
</div>
|
|
69
|
+
))
|
|
70
|
+
: <p class="empty-note">No tickets yet — add them to the Venue document in Studio.</p>}
|
|
71
|
+
</div>
|
|
72
|
+
</div></section>
|
|
73
|
+
</Base>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the pages read.
|
|
3
|
+
*
|
|
4
|
+
* They call the core directly rather than the HTTP API: the site is server-side code in the same
|
|
5
|
+
* process as Trokky, so it has the same trust and needs no token, and a render never leaves the
|
|
6
|
+
* process. Only published content is ever returned from here.
|
|
7
|
+
*
|
|
8
|
+
* Nothing in this file is runtime-specific — that is the point. It is byte-identical on Workers
|
|
9
|
+
* and on Node; only `page.ts` differs, because only the way a page gets hold of the core differs.
|
|
10
|
+
*
|
|
11
|
+
* A conference is joined data — a session points at its track and its speakers — so the whole
|
|
12
|
+
* program is loaded once and resolved here. The Studio writes references as `{_ref, _type}`
|
|
13
|
+
* objects and seeds write plain ids; both must join, which is what `refId` is for.
|
|
14
|
+
*/
|
|
15
|
+
import type { TrokkyCore } from '@trokky/trokky'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where the API is mounted, for the media URLs the browser will fetch. Declared here rather than
|
|
19
|
+
* imported, because the Workers entry that also declares it does not exist on Node.
|
|
20
|
+
*/
|
|
21
|
+
const API_PATH = '/api'
|
|
22
|
+
|
|
23
|
+
type Doc = Record<string, any> & { id: string; _status?: string }
|
|
24
|
+
|
|
25
|
+
const published = { filter: { _status: 'published' } }
|
|
26
|
+
|
|
27
|
+
/** A Studio reference field stores `{_ref, _type}`; seeds and hand-written content store the
|
|
28
|
+
* plain id. The adapter matches on the stored shape, so joins read through this — never `===`. */
|
|
29
|
+
export const refId = (value: unknown): string | null => {
|
|
30
|
+
if (typeof value === 'string') return value
|
|
31
|
+
const ref = (value as { _ref?: unknown } | null)?._ref
|
|
32
|
+
return typeof ref === 'string' ? ref : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A date field is stored exactly as written (`2026-05-13`); the Studio may write a full ISO
|
|
36
|
+
* datetime. Both are the same day, which is what the schedule groups on. */
|
|
37
|
+
const dayOf = (value: unknown): string => (/^\d{4}-\d{2}-\d{2}/.exec(String(value ?? '')) ?? [''])[0]
|
|
38
|
+
|
|
39
|
+
export interface Site {
|
|
40
|
+
event: Doc | null
|
|
41
|
+
venue: Doc | null
|
|
42
|
+
/** Tracks in their scheduled column order. */
|
|
43
|
+
tracks(): Promise<Doc[]>
|
|
44
|
+
track(id: string): Promise<Doc | null>
|
|
45
|
+
/** Speakers in name order. */
|
|
46
|
+
speakers(): Promise<Doc[]>
|
|
47
|
+
/** One speaker, with sessions reverse-resolved from the session documents. */
|
|
48
|
+
speaker(id: string): Promise<Doc | null>
|
|
49
|
+
sessions(opts?: { day?: string; track?: string; speaker?: string; type?: string }): Promise<Doc[]>
|
|
50
|
+
session(id: string): Promise<Doc | null>
|
|
51
|
+
/** Tiers in platinum, gold, silver, partner order; empty tiers are skipped. */
|
|
52
|
+
sponsorsByTier(): Promise<{ tier: string; sponsors: Doc[] }[]>
|
|
53
|
+
/** The distinct session days, in order. */
|
|
54
|
+
days(): Promise<string[]>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const TIERS = ['platinum', 'gold', 'silver', 'partner']
|
|
58
|
+
|
|
59
|
+
export async function site(core: TrokkyCore): Promise<Site> {
|
|
60
|
+
const [event, venue, trackRows, speakerRows, sessionRows, sponsorRows] = await Promise.all([
|
|
61
|
+
core.getDocument('event', 'event') as Promise<Doc | null>,
|
|
62
|
+
core.getDocument('venue', 'venue') as Promise<Doc | null>,
|
|
63
|
+
core.listDocuments('track', published) as Promise<Doc[]>,
|
|
64
|
+
core.listDocuments('speaker', published) as Promise<Doc[]>,
|
|
65
|
+
core.listDocuments('session', published) as Promise<Doc[]>,
|
|
66
|
+
core.listDocuments('sponsor', published) as Promise<Doc[]>,
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
// Name guards: a hand-written document without a name would otherwise throw once it reached a
|
|
70
|
+
// sort key, and one bad doc would 500 every route on the site.
|
|
71
|
+
const tracks = [...trackRows].sort((a, b) =>
|
|
72
|
+
Number(a.order ?? 0) - Number(b.order ?? 0) || String(a.name ?? '').localeCompare(String(b.name ?? '')))
|
|
73
|
+
const tracksById = new Map(tracks.map(t => [t.id, t]))
|
|
74
|
+
|
|
75
|
+
const speakers = [...speakerRows].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? '')))
|
|
76
|
+
const speakersById = new Map(speakers.map(sp => [sp.id, sp]))
|
|
77
|
+
|
|
78
|
+
const resolveSession = (s: Doc): Doc => ({
|
|
79
|
+
...s,
|
|
80
|
+
day: dayOf(s.day),
|
|
81
|
+
track: tracksById.get(refId(s.track) ?? '') ?? null,
|
|
82
|
+
speakers: (s.speakers ?? []).map((v: unknown) => speakersById.get(refId(v) ?? '')).filter((sp): sp is Doc => Boolean(sp)),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const sessions = sessionRows
|
|
86
|
+
.map(resolveSession)
|
|
87
|
+
.sort((a, b) => a.day.localeCompare(b.day) || String(a.start ?? '').localeCompare(String(b.start ?? '')))
|
|
88
|
+
|
|
89
|
+
/** A speaker's sessions, reverse-resolved: the speaker document stores none. */
|
|
90
|
+
const sessionsBySpeaker = new Map<string, Doc[]>()
|
|
91
|
+
for (const s of sessions) for (const sp of s.speakers) {
|
|
92
|
+
if (!sessionsBySpeaker.has(sp.id)) sessionsBySpeaker.set(sp.id, [])
|
|
93
|
+
sessionsBySpeaker.get(sp.id)!.push(s)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sessionById = new Map(sessions.map(s => [s.id, s]))
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
event,
|
|
100
|
+
venue,
|
|
101
|
+
|
|
102
|
+
tracks: async () => tracks,
|
|
103
|
+
track: async id => tracksById.get(id) ?? null,
|
|
104
|
+
|
|
105
|
+
speakers: async () => speakers,
|
|
106
|
+
speaker: async id => {
|
|
107
|
+
const sp = speakersById.get(id)
|
|
108
|
+
return sp ? { ...sp, sessions: sessionsBySpeaker.get(id) ?? [] } : null
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
sessions: async ({ day, track, speaker, type } = {}) => sessions.filter(s =>
|
|
112
|
+
(!day || s.day === day) &&
|
|
113
|
+
(!track || s.track?.id === track) &&
|
|
114
|
+
(!type || s.type === type) &&
|
|
115
|
+
(!speaker || s.speakers.some((sp: Doc) => sp.id === speaker))
|
|
116
|
+
),
|
|
117
|
+
session: async id => sessionById.get(id) ?? null,
|
|
118
|
+
|
|
119
|
+
sponsorsByTier: async () => {
|
|
120
|
+
const tiers = new Map(TIERS.map(t => [t, [] as Doc[]]))
|
|
121
|
+
for (const sp of sponsorRows) if (tiers.has(sp.tier)) tiers.get(sp.tier)!.push(sp)
|
|
122
|
+
return [...tiers.entries()].filter(([, sponsors]) => sponsors.length > 0).map(([tier, sponsors]) => ({ tier, sponsors }))
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// A session with no parseable day gets '' from dayOf; it must not become a phantom column.
|
|
126
|
+
days: async () => [...new Set(sessions.map(s => s.day).filter(Boolean))].sort(),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A media field holds `{ asset: { _ref: <media id> }, alt?, caption? }` — the shape the Studio
|
|
132
|
+
* reads and writes. A bare id is accepted too, for content written by hand.
|
|
133
|
+
*/
|
|
134
|
+
export function mediaId(value: unknown): string | null {
|
|
135
|
+
if (typeof value === 'string') return value
|
|
136
|
+
const ref = (value as { asset?: { _ref?: unknown } } | null)?.asset?._ref
|
|
137
|
+
return typeof ref === 'string' ? ref : null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const mediaUrl = (value: unknown, variant?: 'thumbnail' | 'large'): string | null => {
|
|
141
|
+
const id = mediaId(value)
|
|
142
|
+
if (!id) return null
|
|
143
|
+
return variant ? `${API_PATH}/media/${id}/variants/${variant}` : `${API_PATH}/media/${id}/file`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Formatting and the generated marks, shared by the pages and the components. */
|
|
147
|
+
|
|
148
|
+
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
149
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
150
|
+
|
|
151
|
+
/** `2026-05-13` — or the Studio's datetime form of it — as `Wed 13 May`. */
|
|
152
|
+
export const formatDate = (value: unknown): string => {
|
|
153
|
+
const day = dayOf(value)
|
|
154
|
+
if (!day) return ''
|
|
155
|
+
const d = new Date(`${day}T00:00:00`)
|
|
156
|
+
return `${DAYS[d.getDay()]} ${d.getDate()} ${MONTHS[d.getMonth()]}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const formatDateRange = (start: unknown, end: unknown): string => {
|
|
160
|
+
const s = dayOf(start).slice(0, 10)
|
|
161
|
+
const e = dayOf(end).slice(0, 10)
|
|
162
|
+
if (!s || !e) return ''
|
|
163
|
+
const a = new Date(`${s}T00:00:00`)
|
|
164
|
+
const b = new Date(`${e}T00:00:00`)
|
|
165
|
+
if (a.getMonth() === b.getMonth()) {
|
|
166
|
+
return `${MONTHS[a.getMonth()]} ${a.getDate()}–${b.getDate()}, ${b.getFullYear()}`
|
|
167
|
+
}
|
|
168
|
+
return `${formatDate(s)} – ${formatDate(e)}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The generated marks: speakers and sponsors without an uploaded image render a deterministic
|
|
173
|
+
* monogram instead, seeded from a string so the same name always gets the same color — the
|
|
174
|
+
* site ships no image assets at all.
|
|
175
|
+
*/
|
|
176
|
+
export const hashStr = (s: string): number => [...s].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 0)
|
|
177
|
+
|
|
178
|
+
const MARK_COLORS = ['#C8442B', '#2C6E6A', '#B8860B', '#5B4B8A', '#3D6B9E', '#8A3324', '#4A6741', '#7A4B6E']
|
|
179
|
+
export const colorFor = (s: string): string => MARK_COLORS[hashStr(s) % MARK_COLORS.length]
|
|
180
|
+
|
|
181
|
+
export const initials = (name: string): string =>
|
|
182
|
+
name.split(/\s+/).map(w => w[0]).slice(0, 2).join('').toUpperCase()
|
package/manifest.json
CHANGED
|
@@ -137,6 +137,11 @@
|
|
|
137
137
|
"id": "magazine",
|
|
138
138
|
"title": "Magazine",
|
|
139
139
|
"hint": "Articles, authors, categories, documents, pages."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "conference",
|
|
143
|
+
"title": "Conference",
|
|
144
|
+
"hint": "Event, venue, tracks, speakers, sessions, sponsors."
|
|
140
145
|
}
|
|
141
146
|
]
|
|
142
147
|
}
|
|
@@ -225,10 +230,11 @@
|
|
|
225
230
|
},
|
|
226
231
|
"allow": {
|
|
227
232
|
"content": [
|
|
228
|
-
"magazine"
|
|
233
|
+
"magazine",
|
|
234
|
+
"conference"
|
|
229
235
|
]
|
|
230
236
|
},
|
|
231
|
-
"because": "A site needs something to render. Choose the magazine content model, or drop the frontend."
|
|
237
|
+
"because": "A site needs something to render. Choose the magazine or conference content model, or drop the frontend."
|
|
232
238
|
}
|
|
233
239
|
],
|
|
234
240
|
"defaults": {
|
|
@@ -326,6 +332,21 @@
|
|
|
326
332
|
"parts": "full-site",
|
|
327
333
|
"content": "magazine"
|
|
328
334
|
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "conference",
|
|
338
|
+
"title": "The conference template",
|
|
339
|
+
"repo": "https://github.com/Trokky/templates",
|
|
340
|
+
"path": "conference",
|
|
341
|
+
"deployButton": "https://deploy.workers.cloudflare.com/?url=https://github.com/Trokky/templates/tree/main/conference",
|
|
342
|
+
"composition": {
|
|
343
|
+
"target": "workers",
|
|
344
|
+
"data": "cloudflare-d1",
|
|
345
|
+
"media": "cloudflare-r2",
|
|
346
|
+
"images": "cloudflare-images",
|
|
347
|
+
"parts": "full-site",
|
|
348
|
+
"content": "conference"
|
|
349
|
+
}
|
|
329
350
|
}
|
|
330
351
|
]
|
|
331
352
|
}
|