@acms-subir/core 0.2.0 → 0.3.1
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/CHANGELOG.md +87 -0
- package/cli/common.mjs +35 -0
- package/cli/legacy.mjs +317 -0
- package/cli/main.mjs +66 -0
- package/cli/palette.mjs +4 -0
- package/cli/preview.mjs +50 -0
- package/cli/remote.mjs +55 -0
- package/cli/themes.mjs +127 -0
- package/cli.mjs +2 -369
- package/deploy/Caddyfile +8 -0
- package/deploy/Dockerfile +34 -0
- package/deploy/compose.https.yml +21 -0
- package/deploy/compose.yml +46 -0
- package/docs/live-video.md +91 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +189 -2
- package/ops-check.mjs +27 -0
- package/package.json +27 -5
- package/src/actions/index.ts +578 -49
- package/src/components/AiCleaner.tsx +291 -0
- package/src/components/AiSettings.tsx +575 -0
- package/src/components/CommandPalette.tsx +200 -0
- package/src/components/ContentList.astro +246 -0
- package/src/components/DesignAccess.tsx +182 -0
- package/src/components/DesignManager.tsx +418 -46
- package/src/components/EmailSettings.tsx +521 -0
- package/src/components/EventPage.astro +178 -0
- package/src/components/LegalPage.astro +27 -0
- package/src/components/LinksManager.tsx +119 -58
- package/src/components/LiveCard.astro +174 -0
- package/src/components/LiveManager.tsx +910 -0
- package/src/components/NavManager.tsx +42 -43
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +58 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/WidgetRenderer.astro +3 -0
- package/src/components/booking/BookingFlow.astro +150 -0
- package/src/components/booking/HoursEditor.astro +63 -0
- package/src/components/builder/PageBuilder.tsx +10 -2
- package/src/components/editor/EventPanel.tsx +401 -0
- package/src/components/editor/FormatDock.tsx +81 -0
- package/src/components/editor/LivePane.tsx +122 -0
- package/src/components/editor/MobileToolbar.tsx +93 -0
- package/src/components/editor/PostEditor.tsx +605 -223
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/slash.tsx +7 -1
- package/src/components/site/Pager.astro +24 -0
- package/src/components/site/PostCard.astro +64 -0
- package/src/components/site/TagLabel.astro +11 -0
- package/src/layouts/Admin.astro +289 -49
- package/src/layouts/Site.astro +352 -59
- package/src/lib/agent-api.ts +13 -0
- package/src/lib/agent-discovery.ts +40 -0
- package/src/lib/agent.ts +114 -0
- package/src/lib/ai-config.ts +58 -0
- package/src/lib/ai-schema.ts +22 -0
- package/src/lib/ai.ts +90 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +56 -0
- package/src/lib/auth.ts +25 -5
- package/src/lib/backup/content.ts +36 -0
- package/src/lib/backup/unzip.ts +12 -1
- package/src/lib/backup/zip.ts +1 -1
- package/src/lib/booking/agent.ts +108 -0
- package/src/lib/booking/availability.ts +221 -0
- package/src/lib/booking/emails.ts +115 -0
- package/src/lib/booking/forms.ts +77 -0
- package/src/lib/booking/service.ts +277 -0
- package/src/lib/booking/view.ts +54 -0
- package/src/lib/calendar/agent-schema.ts +48 -0
- package/src/lib/calendar/agent.ts +136 -0
- package/src/lib/calendar/api.ts +81 -0
- package/src/lib/calendar/cards.ts +102 -0
- package/src/lib/calendar/config.ts +52 -0
- package/src/lib/calendar/data.ts +130 -0
- package/src/lib/calendar/events.ts +261 -0
- package/src/lib/calendar/feeds.ts +43 -0
- package/src/lib/calendar/hooks.ts +38 -0
- package/src/lib/calendar/http.ts +30 -0
- package/src/lib/calendar/ics-write.ts +120 -0
- package/src/lib/calendar/ics.ts +15 -5
- package/src/lib/calendar/import.ts +203 -0
- package/src/lib/calendar/render.ts +120 -37
- package/src/lib/calendar/rrule.ts +149 -0
- package/src/lib/calendar/seo.ts +48 -0
- package/src/lib/design-service.ts +58 -0
- package/src/lib/designDiff.ts +145 -0
- package/src/lib/designPreview.ts +110 -12
- package/src/lib/documents/cleanup.ts +31 -0
- package/src/lib/documents/extract.ts +154 -0
- package/src/lib/documents/vendor.d.ts +4 -0
- package/src/lib/drafts.ts +60 -6
- package/src/lib/dynamicBlocks.ts +40 -8
- package/src/lib/email/accounts.ts +55 -0
- package/src/lib/email/config.ts +86 -0
- package/src/lib/email/migration.ts +24 -0
- package/src/lib/email/providers.ts +89 -0
- package/src/lib/email/service.ts +78 -0
- package/src/lib/email/store.ts +167 -0
- package/src/lib/email/templates.ts +18 -0
- package/src/lib/email/types.ts +32 -0
- package/src/lib/email/webhooks.ts +87 -0
- package/src/lib/email.ts +15 -0
- package/src/lib/env.ts +1 -1
- package/src/lib/legal/index.ts +25 -0
- package/src/lib/live/client.ts +88 -0
- package/src/lib/live/crypto.ts +28 -0
- package/src/lib/live/migration.ts +15 -0
- package/src/lib/live/providers.ts +152 -0
- package/src/lib/live/service.ts +170 -0
- package/src/lib/live/signals.ts +89 -0
- package/src/lib/live/store.ts +49 -0
- package/src/lib/live/types.ts +52 -0
- package/src/lib/live/view.ts +78 -0
- package/src/lib/mobile/bookings.ts +73 -0
- package/src/lib/mobile/calendar.ts +85 -0
- package/src/lib/mobile/content.ts +85 -0
- package/src/lib/mobile/http.ts +63 -0
- package/src/lib/mobile/route.ts +20 -0
- package/src/lib/ops.ts +8 -0
- package/src/lib/preview.ts +21 -1
- package/src/lib/repo/migrations.ts +221 -0
- package/src/lib/repo/sql.ts +907 -68
- package/src/lib/repo/types.ts +125 -5
- package/src/lib/scheduler.ts +27 -0
- package/src/lib/schema.ts +316 -1
- package/src/lib/search-i18n.ts +3 -0
- package/src/lib/socialIcons.ts +32 -0
- package/src/lib/tagColor.ts +11 -0
- package/src/lib/theme.ts +59 -40
- package/src/lib/themeTokens.ts +183 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhook-sql.ts +68 -0
- package/src/lib/webhooks.ts +147 -0
- package/src/lib/widgets.ts +1 -0
- package/src/middleware.ts +45 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +14 -5
- package/src/pages/account/[action].astro +53 -0
- package/src/pages/admin/ai.astro +12 -0
- package/src/pages/admin/c/[collection]/[id].astro +4 -1
- package/src/pages/admin/c/[collection]/index.astro +127 -92
- package/src/pages/admin/calendar/bookings.astro +400 -0
- package/src/pages/admin/calendar/calendars.astro +265 -0
- package/src/pages/admin/calendar/events/[id].astro +56 -0
- package/src/pages/admin/calendar/events/index.astro +408 -0
- package/src/pages/admin/calendar/index.ts +4 -0
- package/src/pages/admin/calendar/integrations.astro +4 -0
- package/src/pages/admin/calendar/resources/[id].astro +180 -0
- package/src/pages/admin/calendar/resources/index.astro +106 -0
- package/src/pages/admin/calendar/resources/link.astro +196 -0
- package/src/pages/admin/design.astro +10 -3
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +232 -85
- package/src/pages/admin/integrations.astro +237 -0
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/live.astro +12 -0
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +4 -1
- package/src/pages/admin/pages/index.astro +41 -48
- package/src/pages/admin/settings.astro +418 -367
- package/src/pages/admin/tags.astro +4 -4
- package/src/pages/admin/users.astro +38 -28
- package/src/pages/api/agent/docs.ts +33 -0
- package/src/pages/api/agent/index.ts +21 -0
- package/src/pages/api/ai/clean.ts +13 -0
- package/src/pages/api/ai/draft.ts +14 -0
- package/src/pages/api/ai/settings.ts +53 -0
- package/src/pages/api/ai/test.ts +12 -0
- package/src/pages/api/backup.ts +8 -21
- package/src/pages/api/calendar/availability.ts +35 -0
- package/src/pages/api/calendar/bookings/[id].ts +49 -0
- package/src/pages/api/calendar/bookings/index.ts +73 -0
- package/src/pages/api/calendar/calendars.ts +35 -0
- package/src/pages/api/calendar/docs.ts +66 -0
- package/src/pages/api/calendar/events/[id].ts +63 -0
- package/src/pages/api/calendar/events/index.ts +112 -0
- package/src/pages/api/calendar/resources.ts +34 -0
- package/src/pages/api/design/capabilities.ts +10 -0
- package/src/pages/api/design/index.ts +58 -0
- package/src/pages/api/design/keys.ts +24 -0
- package/src/pages/api/design/media.ts +32 -0
- package/src/pages/api/email/run.ts +11 -0
- package/src/pages/api/email/settings.ts +61 -0
- package/src/pages/api/email/webhooks/[provider].ts +4 -0
- package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
- package/src/pages/api/live/settings.ts +45 -0
- package/src/pages/api/live/status.ts +7 -0
- package/src/pages/api/live/thumb.ts +48 -0
- package/src/pages/api/mcp.ts +59 -0
- package/src/pages/api/media/upload.ts +2 -2
- package/src/pages/api/ops/status.ts +39 -0
- package/src/pages/api/restore.ts +27 -37
- package/src/pages/api/search.ts +18 -4
- package/src/pages/api/v1/availability.ts +7 -0
- package/src/pages/api/v1/bookings.ts +17 -0
- package/src/pages/api/v1/calendars.ts +7 -0
- package/src/pages/api/v1/content/[id].ts +7 -0
- package/src/pages/api/v1/content/index.ts +7 -0
- package/src/pages/api/v1/events.ts +7 -0
- package/src/pages/api/v1/resources.ts +7 -0
- package/src/pages/api/webhooks/docs.ts +61 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +137 -66
- package/src/pages/blog/index.astro +95 -30
- package/src/pages/blog/tag/[slug].astro +45 -40
- package/src/pages/book/[resource]/[link].astro +70 -0
- package/src/pages/book/[resource]/index.astro +63 -0
- package/src/pages/book/[resource].ics.ts +38 -0
- package/src/pages/book/index.astro +47 -0
- package/src/pages/book/manage/[token].astro +108 -0
- package/src/pages/calendar/[slug].ics.ts +27 -0
- package/src/pages/calendar/feed.ics.ts +15 -0
- package/src/pages/calendar/index.astro +191 -0
- package/src/pages/embed/book/[resource]/[link].astro +33 -0
- package/src/pages/embed/calendar/[slug].astro +43 -0
- package/src/pages/events/[slug].astro +74 -0
- package/src/pages/events/[slug].ics.ts +25 -0
- package/src/pages/index.astro +12 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +29 -3
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +27 -6
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +924 -105
- package/src/styles/cards.css +92 -74
- package/src/styles/editor.css +492 -114
- package/src/styles/email-settings.css +117 -0
- package/src/styles/live-admin.css +126 -0
- package/src/styles/live.css +199 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -281
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +81 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +55 -0
- package/src/themes/editorial/templates/home.astro +4 -4
- package/src/themes/editorial/templates/post-minimal.astro +2 -2
- package/src/themes/editorial/theme.css +27 -37
- package/src/themes/editorial/theme.json +25 -3
- package/src/themes/field/theme.json +27 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +28 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The built-in event page (feature image, date block, place, button, add to
|
|
3
|
+
// calendar, share, related events). Themes replace it with
|
|
4
|
+
// templates/event.astro, which receives the same props. Every element
|
|
5
|
+
// carries a stable .pc-ev-* class for CSS overrides.
|
|
6
|
+
import type { CalEvent, Calendar, Settings } from '../lib/schema';
|
|
7
|
+
import { CALENDAR, eventIcsUrl, eventUrl, calendarPageUrl } from '../lib/calendar/config';
|
|
8
|
+
import { addToCalendarLinks, eventTimeZone, formatOccurrence, safeColor, type Occurrence } from '../lib/calendar/events';
|
|
9
|
+
import { describeRepeat } from '../lib/calendar/rrule';
|
|
10
|
+
import { eventCardHtml, safeHref } from '../lib/calendar/cards';
|
|
11
|
+
import { siteLocale } from '../lib/dates';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
event: CalEvent;
|
|
15
|
+
occurrence: Occurrence;
|
|
16
|
+
settings: Settings;
|
|
17
|
+
calendars: Calendar[];
|
|
18
|
+
html: string;
|
|
19
|
+
upcoming: Pick<Occurrence, 'event' | 'start' | 'end' | 'allDay' | 'days'>[];
|
|
20
|
+
origin: string;
|
|
21
|
+
preview?: boolean;
|
|
22
|
+
// RSVP state when the event takes RSVPs (null otherwise)
|
|
23
|
+
rsvp?: { going: number; left: number | null; maxSeats: number; error: string; open: boolean } | null;
|
|
24
|
+
}
|
|
25
|
+
const { event: e, occurrence: o, settings, calendars, html, upcoming, origin, preview = false, rsvp = null } = Astro.props;
|
|
26
|
+
const tz = eventTimeZone(e, settings);
|
|
27
|
+
const locale = siteLocale(settings);
|
|
28
|
+
const f = formatOccurrence(o, tz, locale);
|
|
29
|
+
const pageUrl = origin + eventUrl(e.slug);
|
|
30
|
+
const links = addToCalendarLinks(e, o, { pageUrl, icsUrl: eventIcsUrl(e.slug), tz });
|
|
31
|
+
const repeats = describeRepeat(e.rrule, e.start, (ymd) => {
|
|
32
|
+
const [y, m, d] = ymd.split('-').map(Number);
|
|
33
|
+
return new Date(Date.UTC(y, m - 1, d)).toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });
|
|
34
|
+
});
|
|
35
|
+
const ctaHref = safeHref(e.cta.href);
|
|
36
|
+
const mapHref = e.location.url
|
|
37
|
+
? safeHref(e.location.url)
|
|
38
|
+
: e.location.address
|
|
39
|
+
? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent([e.location.name, e.location.address].filter(Boolean).join(', '))}`
|
|
40
|
+
: '';
|
|
41
|
+
const fieldRows = CALENDAR.eventFields.filter((fd) => e.fields[fd.key]).map((fd) => ({ label: fd.label, value: e.fields[fd.key], type: fd.type }));
|
|
42
|
+
const visibleTags = e.tags.filter((t) => !t.startsWith('#'));
|
|
43
|
+
const I = {
|
|
44
|
+
pin: 'M12 22s7-7 7-12a7 7 0 0 0-14 0c0 5 7 12 7 12zM12 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
|
|
45
|
+
clock: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2',
|
|
46
|
+
repeat: 'M17 2l4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3',
|
|
47
|
+
cal: 'M3 5.5h18v15H3zM3 10h18M8 3v4M16 3v4',
|
|
48
|
+
share: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1.5-1.5',
|
|
49
|
+
info: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 11v5M12 8h.01',
|
|
50
|
+
video: 'M3 7h12v10H3zM15 10l6-3v10l-6-3',
|
|
51
|
+
};
|
|
52
|
+
const online = /zoom\.us|meet\.google|teams\.microsoft|webex|whereby|jit\.si/i.test(e.location.url);
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<article class={`pc-ev-page${e.cancelled ? ' is-cancelled' : ''}`}>
|
|
56
|
+
{preview && <p class="pc-ev-preview">Preview — only you can see this {e.status === 'published' ? 'version' : 'draft'}.</p>}
|
|
57
|
+
{e.cover && (
|
|
58
|
+
<figure class="pc-ev-hero"><img src={e.cover} alt={e.coverAlt} /></figure>
|
|
59
|
+
)}
|
|
60
|
+
<div class="pc-ev-layout">
|
|
61
|
+
<div class="pc-ev-main">
|
|
62
|
+
{(calendars.length > 0 || visibleTags.length > 0) && (
|
|
63
|
+
<div class="pc-ev-chips">
|
|
64
|
+
{calendars.map((c) => (
|
|
65
|
+
<a class="pc-ev-chip" href={calendarPageUrl(c.slug)}><i style={`background:${safeColor(c.color)}`} aria-hidden="true" />{c.name}</a>
|
|
66
|
+
))}
|
|
67
|
+
{visibleTags.map((t) => <span class="pc-ev-chip is-tag">{t}</span>)}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
<h1 class="pc-ev-title">{e.title}</h1>
|
|
71
|
+
{e.cancelled && <p class="pc-ev-cancelled"><b>This event is cancelled.</b> It stays listed so nobody is left wondering.</p>}
|
|
72
|
+
{e.excerpt && <p class="pc-ev-lede">{e.excerpt}</p>}
|
|
73
|
+
<div class="pc-ev-body post-content" set:html={html} />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<aside class="pc-ev-side" aria-label="Event details">
|
|
77
|
+
<div class="pc-ev-date">
|
|
78
|
+
<span class="pc-ev-tile" aria-hidden="true"><b>{f.month}</b><span>{f.day}</span></span>
|
|
79
|
+
<div>
|
|
80
|
+
<b>{f.date}</b>
|
|
81
|
+
<span>{f.time}</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{(e.location.name || e.location.address || e.location.url) && (
|
|
85
|
+
<div class="pc-ev-fact">
|
|
86
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={online && !e.location.address ? I.video : I.pin} /></svg>
|
|
87
|
+
<div>
|
|
88
|
+
{e.location.name && <b>{e.location.name}</b>}
|
|
89
|
+
{(e.location.address || mapHref) && (
|
|
90
|
+
<span>
|
|
91
|
+
{e.location.address}
|
|
92
|
+
{mapHref && <>{e.location.address && ' · '}<a href={mapHref} target="_blank" rel="noopener">{online ? 'Join online' : 'Map'}</a></>}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
{repeats && (
|
|
99
|
+
<div class="pc-ev-fact">
|
|
100
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={I.repeat} /></svg>
|
|
101
|
+
<div><b>{repeats}</b><span>Showing {f.date}</span></div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
{fieldRows.map((r) => (
|
|
105
|
+
<div class="pc-ev-fact">
|
|
106
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={I.info} /></svg>
|
|
107
|
+
<div><span>{r.label}</span><b>{r.type === 'url' && safeHref(r.value) ? <a href={safeHref(r.value)} target="_blank" rel="noopener">{r.value.replace(/^https?:\/\//, '')}</a> : r.value}</b></div>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
{rsvp ? (
|
|
111
|
+
<div class="pc-ev-rsvp">
|
|
112
|
+
<p class="pc-ev-going"><b>{rsvp.going}</b> going{rsvp.left !== null && <> · <b>{rsvp.left}</b> {rsvp.left === 1 ? 'spot' : 'spots'} left</>}</p>
|
|
113
|
+
{rsvp.open && (rsvp.left === null || rsvp.left > 0) ? (
|
|
114
|
+
<details class="pc-ev-rsvpform" open={Boolean(rsvp.error)}>
|
|
115
|
+
<summary class="pc-ev-cta">Save my seat</summary>
|
|
116
|
+
<form method="post">
|
|
117
|
+
{rsvp.error && <p class="pc-bk-error">{rsvp.error}</p>}
|
|
118
|
+
<label class="pc-bk-field"><span>Your name</span><input name="name" required autocomplete="name" /></label>
|
|
119
|
+
<label class="pc-bk-field"><span>Email</span><input name="email" type="email" required autocomplete="email" /></label>
|
|
120
|
+
{rsvp.maxSeats > 1 && (
|
|
121
|
+
<label class="pc-bk-field"><span>Seats</span>
|
|
122
|
+
<select name="seats">{Array.from({ length: Math.min(rsvp.maxSeats, rsvp.left ?? rsvp.maxSeats) }, (_, i) => <option value={i + 1}>{i + 1}</option>)}</select>
|
|
123
|
+
</label>
|
|
124
|
+
)}
|
|
125
|
+
<button class="pc-bk-submit" type="submit">{e.rsvp.autoConfirm ? 'Confirm my seat' : 'Request a seat'}</button>
|
|
126
|
+
</form>
|
|
127
|
+
</details>
|
|
128
|
+
) : <p class="pc-ev-full">{rsvp.open ? 'This event is full.' : 'RSVPs are closed.'}</p>}
|
|
129
|
+
</div>
|
|
130
|
+
) : e.cta.label && ctaHref && !e.cancelled && (
|
|
131
|
+
<a class={`pc-ev-cta${e.cta.style === 'outline' ? ' is-outline' : ''}`} href={ctaHref}>{e.cta.label}</a>
|
|
132
|
+
)}
|
|
133
|
+
<details class="pc-ev-add">
|
|
134
|
+
<summary>
|
|
135
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={I.cal} /></svg>
|
|
136
|
+
Add to calendar
|
|
137
|
+
</summary>
|
|
138
|
+
<div class="pc-ev-add-menu">
|
|
139
|
+
<a href={links.google} target="_blank" rel="noopener">Google Calendar</a>
|
|
140
|
+
<a href={links.ics}>Apple Calendar</a>
|
|
141
|
+
<a href={links.outlook} target="_blank" rel="noopener">Outlook</a>
|
|
142
|
+
<a href={links.ics} download={`${e.slug}.ics`}>Download .ics</a>
|
|
143
|
+
</div>
|
|
144
|
+
</details>
|
|
145
|
+
<button type="button" class="pc-ev-share" data-url={pageUrl} data-title={e.title}>
|
|
146
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={I.share} /></svg>
|
|
147
|
+
<span>Share</span>
|
|
148
|
+
</button>
|
|
149
|
+
</aside>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{upcoming.length > 0 && (
|
|
153
|
+
<section class="pc-ev-more">
|
|
154
|
+
<h2>More events</h2>
|
|
155
|
+
<div class="pc-ev-cards" data-layout="grid" set:html={upcoming.map((u) => eventCardHtml(u, settings, { showImage: true })).join('')} />
|
|
156
|
+
<a class="pc-ev-all" href={CALENDAR.base}>See the full calendar →</a>
|
|
157
|
+
</section>
|
|
158
|
+
)}
|
|
159
|
+
</article>
|
|
160
|
+
|
|
161
|
+
<script>
|
|
162
|
+
// share sheet where available, else copy the link
|
|
163
|
+
document.querySelectorAll<HTMLButtonElement>('.pc-ev-share').forEach((b) => b.addEventListener('click', async () => {
|
|
164
|
+
const url = b.dataset.url ?? location.href;
|
|
165
|
+
if (navigator.share) {
|
|
166
|
+
try { await navigator.share({ title: b.dataset.title, url }); return; } catch { /* dismissed */ }
|
|
167
|
+
}
|
|
168
|
+
await navigator.clipboard.writeText(url);
|
|
169
|
+
const label = b.querySelector('span');
|
|
170
|
+
if (label) { label.textContent = 'Link copied'; setTimeout(() => (label.textContent = 'Share'), 1400); }
|
|
171
|
+
}));
|
|
172
|
+
// one open menu; outside click closes
|
|
173
|
+
document.addEventListener('click', (ev) => {
|
|
174
|
+
document.querySelectorAll<HTMLDetailsElement>('details.pc-ev-add[open]').forEach((d) => {
|
|
175
|
+
if (!(ev.target instanceof Node && d.contains(ev.target))) d.open = false;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Site from '../layouts/Site.astro';
|
|
3
|
+
import WidgetRenderer from './WidgetRenderer.astro';
|
|
4
|
+
import { getRepo } from '../lib/repo';
|
|
5
|
+
import { defaultPolicy, LEGAL } from '../lib/legal';
|
|
6
|
+
import { expandDynamicBlocks } from '../lib/dynamicBlocks';
|
|
7
|
+
interface Props { policy: 'privacy' | 'terms' }
|
|
8
|
+
const { policy } = Astro.props;
|
|
9
|
+
const repo = await getRepo(Astro.locals);
|
|
10
|
+
const [settings, page] = await Promise.all([repo.getSettings(), repo.getPageBySlug(policy)]);
|
|
11
|
+
const custom = page?.status === 'published' ? page : null;
|
|
12
|
+
const standard = defaultPolicy(policy, settings);
|
|
13
|
+
const html = custom?.bodyHtml ? await expandDynamicBlocks(custom.bodyHtml, repo) : '';
|
|
14
|
+
---
|
|
15
|
+
<Site title={custom?.title || standard.title} noindex={!custom && !standard.complete}>
|
|
16
|
+
{custom ? (
|
|
17
|
+
html ? <article class="page-body" set:html={html} /> : <WidgetRenderer pageId={custom.id} blocks={custom.blocks} />
|
|
18
|
+
) : (
|
|
19
|
+
<article class="page-body">
|
|
20
|
+
<h1>{standard.title}</h1>
|
|
21
|
+
{LEGAL.updatedAt && <p>Last updated: <time datetime={LEGAL.updatedAt}>{LEGAL.updatedAt}</time></p>}
|
|
22
|
+
{!standard.complete && <p><strong>Standard notice:</strong> The site operator has not yet completed its identity and contact details. This page describes the standard site features and needs the operator’s site-specific information.</p>}
|
|
23
|
+
{standard.sections.map(section => <section><h2>{section.title}</h2><p>{section.text}</p></section>)}
|
|
24
|
+
{LEGAL.contactEmail && <p><a href={`mailto:${LEGAL.contactEmail}`}>Contact the site operator</a></p>}
|
|
25
|
+
</article>
|
|
26
|
+
)}
|
|
27
|
+
</Site>
|
|
@@ -61,6 +61,7 @@ const SITES = [
|
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
const TILE_ICONS: Record<string, string> = {
|
|
64
|
+
live: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 5h12v14H3zM15 10l6-4v12l-6-4"/></svg>',
|
|
64
65
|
site: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 3l4 2.3v4.4L12 12 8 9.7V5.3L12 3zM6 13l4 2.3v4.4L6 22l-4-2.3v-4.4L6 13zM18 13l4 2.3v4.4L18 22l-4-2.3v-4.4L18 13z"/></svg>',
|
|
65
66
|
link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M10 14a5 5 0 007.07 0l2.83-2.83a5 5 0 00-7.07-7.07L11 5.93M14 10a5 5 0 00-7.07 0L4.1 12.83a5 5 0 007.07 7.07L13 18.07"/></svg>',
|
|
66
67
|
email: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>',
|
|
@@ -87,6 +88,7 @@ const TILE_GROUPS: { label: string; tiles: Tile[] }[] = [
|
|
|
87
88
|
{
|
|
88
89
|
label: 'Content & layout',
|
|
89
90
|
tiles: [
|
|
91
|
+
{ key: 'live', kind: 'live', name: 'Live video', desc: 'Your shared live broadcast' },
|
|
90
92
|
{ key: 'heading', kind: 'heading', name: 'Heading', desc: 'Group your links' },
|
|
91
93
|
{ key: 'text', kind: 'text', name: 'Text', desc: 'Non-clickable copy' },
|
|
92
94
|
{ key: 'spacer', kind: 'spacer', name: 'Spacer', desc: 'Blank vertical space' },
|
|
@@ -96,6 +98,7 @@ const TILE_GROUPS: { label: string; tiles: Tile[] }[] = [
|
|
|
96
98
|
];
|
|
97
99
|
|
|
98
100
|
const NEW_ITEM: Record<Kind, Partial<Link>> = {
|
|
101
|
+
live: { kind: 'live', label: 'Live video', url: '' },
|
|
99
102
|
link: { kind: 'link', label: 'New link', url: '', icon: '' },
|
|
100
103
|
email: { kind: 'email', label: 'E-mail me', url: '', icon: '✉️' },
|
|
101
104
|
phone: { kind: 'phone', label: 'Call me', url: '', icon: '📞' },
|
|
@@ -128,6 +131,8 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
128
131
|
const [colorPickerFor, setColorPickerFor] = useState<string | null>(null);
|
|
129
132
|
const [saveState, setSaveState] = useState<SaveState>('saved');
|
|
130
133
|
const [tick, setTick] = useState(0);
|
|
134
|
+
// narrow screens show one pane at a time
|
|
135
|
+
const [view, setView] = useState<'edit' | 'preview' | 'insights'>('edit');
|
|
131
136
|
|
|
132
137
|
const linkShape = (l: Link) => ({ id: l.id, kind: l.kind, label: l.label, url: l.url, icon: l.icon, html: l.html, meta: l.meta });
|
|
133
138
|
const dirty = useMemo(
|
|
@@ -246,10 +251,21 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
246
251
|
saveState === 'error' ? 'Save failed' :
|
|
247
252
|
dirty ? 'Unsaved changes — not live yet' : 'Live';
|
|
248
253
|
|
|
254
|
+
const clickable = links.filter((l) => l.kind === 'link' || l.kind === 'email' || l.kind === 'phone' || l.kind === 'vcard');
|
|
255
|
+
const totalClicks = clickable.reduce((n, l) => n + l.clicks, 0);
|
|
256
|
+
const topClicks = [...clickable].sort((x, y) => y.clicks - x.clicks);
|
|
257
|
+
|
|
249
258
|
return (
|
|
250
|
-
<div className=
|
|
251
|
-
<div>
|
|
252
|
-
|
|
259
|
+
<div className={`lm-layout view-${view}`}>
|
|
260
|
+
<div className="lm-views seg" role="tablist" aria-label="Links page view">
|
|
261
|
+
{(['edit', 'preview', 'insights'] as const).map((v) => (
|
|
262
|
+
<button key={v} type="button" role="tab" aria-selected={view === v} onClick={() => { setView(v); if (v === 'preview') setTick((t) => t + 1); }}>
|
|
263
|
+
{v[0].toUpperCase() + v.slice(1)}
|
|
264
|
+
</button>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
<div className="lm-editor">
|
|
268
|
+
<div className="lm-profile">
|
|
253
269
|
<div className="lm-avatarwrap">
|
|
254
270
|
<button
|
|
255
271
|
type="button"
|
|
@@ -260,7 +276,7 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
260
276
|
{profile.linksAvatar ? (
|
|
261
277
|
<img src={profile.linksAvatar} alt="" />
|
|
262
278
|
) : (
|
|
263
|
-
<
|
|
279
|
+
<svg aria-hidden="true" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M4 8h3l2-3h6l2 3h3v11H4zM12 11a3 3 0 1 0 0 6 3 3 0 0 0 0-6z" /></svg>
|
|
264
280
|
)}
|
|
265
281
|
<span className="lm-avatar-hint">Edit</span>
|
|
266
282
|
</button>
|
|
@@ -299,50 +315,60 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
299
315
|
</div>
|
|
300
316
|
</div>
|
|
301
317
|
|
|
302
|
-
<div className="
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
{pickerOpen
|
|
306
|
-
|
|
307
|
-
)}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
</
|
|
312
|
-
{pickerOpen &&
|
|
313
|
-
<div
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
</
|
|
318
|
+
<div className="lm-addwrap">
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className={`lm-addbtn ${pickerOpen ? 'is-open' : ''}`}
|
|
322
|
+
aria-expanded={pickerOpen}
|
|
323
|
+
onClick={() => { setPickerOpen((v) => !v); setSitesOpen(false); }}
|
|
324
|
+
>
|
|
325
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>
|
|
326
|
+
Add block
|
|
327
|
+
</button>
|
|
328
|
+
{pickerOpen && (
|
|
329
|
+
<div className="lm-picker">
|
|
330
|
+
{sitesOpen && (
|
|
331
|
+
<div className="lm-picker-head">
|
|
332
|
+
<button type="button" className="btn sm ghost" onClick={() => setSitesOpen(false)}>‹ All blocks</button>
|
|
333
|
+
<b>Choose a site</b>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{!sitesOpen && TILE_GROUPS.map((g) => (
|
|
337
|
+
<div key={g.label}>
|
|
338
|
+
<p className="lm-group-label">{g.label}</p>
|
|
339
|
+
<div className="lm-tiles">
|
|
340
|
+
{g.tiles.map((t) => (
|
|
341
|
+
<button
|
|
342
|
+
key={t.key}
|
|
343
|
+
type="button"
|
|
344
|
+
className="lm-tile"
|
|
345
|
+
onClick={() => (t.kind === 'site' ? setSitesOpen(true) : add(NEW_ITEM[t.kind]))}
|
|
346
|
+
>
|
|
347
|
+
<span className="lm-tile-ico" dangerouslySetInnerHTML={{ __html: TILE_ICONS[t.key] }} />
|
|
348
|
+
<span className="lm-tile-body">
|
|
349
|
+
<b>{t.name}</b>
|
|
350
|
+
<span>{t.desc}</span>
|
|
351
|
+
</span>
|
|
352
|
+
</button>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
340
356
|
))}
|
|
357
|
+
{sitesOpen && (
|
|
358
|
+
<div className="lm-sites">
|
|
359
|
+
{SITES.map((s) => (
|
|
360
|
+
<button key={s.name} type="button" className="lm-site" onClick={() => addSite(s)} style={{ color: `#${s.si.hex}` }}>
|
|
361
|
+
<span className="lm-site-ico" dangerouslySetInnerHTML={{ __html: svgOf(s.si) }} />
|
|
362
|
+
<span>{s.name}</span>
|
|
363
|
+
</button>
|
|
364
|
+
))}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
341
367
|
</div>
|
|
342
368
|
)}
|
|
343
369
|
</div>
|
|
344
370
|
|
|
345
|
-
<div className="
|
|
371
|
+
<div className="lm-list rowlist">
|
|
346
372
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
347
373
|
<SortableContext items={links.map((l) => l.id)} strategy={verticalListSortingStrategy}>
|
|
348
374
|
{links.map((l) => (
|
|
@@ -361,7 +387,7 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
361
387
|
))}
|
|
362
388
|
</SortableContext>
|
|
363
389
|
</DndContext>
|
|
364
|
-
{links.length === 0 && <p className="hint empty">Nothing here yet — add your first block above.</p>}
|
|
390
|
+
{links.length === 0 && <p className="hint lm-empty">Nothing here yet — add your first block above.</p>}
|
|
365
391
|
</div>
|
|
366
392
|
</div>
|
|
367
393
|
|
|
@@ -377,23 +403,47 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
377
403
|
|
|
378
404
|
<aside className="lm-preview">
|
|
379
405
|
<div className="lm-preview-bar">
|
|
380
|
-
<span className="lm-
|
|
381
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
406
|
+
<span className="lm-url">{location.host}/links</span>
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
className="icon-btn"
|
|
410
|
+
title="Copy link"
|
|
411
|
+
aria-label="Copy link"
|
|
412
|
+
onClick={() => void navigator.clipboard.writeText(`${location.origin}/links`)}
|
|
413
|
+
>
|
|
414
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M9 9h11v11H9zM5 15H4V4h11v1" /></svg>
|
|
415
|
+
</button>
|
|
416
|
+
<a className="icon-btn" href="/links" target="_blank" rel="noopener" title="Open in tab" aria-label="Open in tab">
|
|
417
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" /></svg>
|
|
418
|
+
</a>
|
|
392
419
|
</div>
|
|
393
420
|
<div className="lm-phone">
|
|
394
421
|
<iframe key={tick} src="/links" title="Links page preview" />
|
|
395
422
|
</div>
|
|
423
|
+
<p className="lm-stat"><b>{totalClicks.toLocaleString()}</b> total clicks · <span className={`lm-savestate ${saveState === 'saved' && dirty ? 'dirty' : saveState}`}>{saveLabel}</span></p>
|
|
396
424
|
</aside>
|
|
425
|
+
|
|
426
|
+
<section className="lm-insights" aria-label="Insights">
|
|
427
|
+
<div className="card-head"><b>Clicks by block</b><span className="hint">{totalClicks.toLocaleString()} total</span></div>
|
|
428
|
+
{topClicks.length === 0 && <p className="hint">No buttons yet.</p>}
|
|
429
|
+
{topClicks.map((l) => (
|
|
430
|
+
<div key={l.id} className="mini-row bar-row">
|
|
431
|
+
<i style={{ width: `${topClicks[0].clicks ? (l.clicks / topClicks[0].clicks) * 100 : 0}%` }} />
|
|
432
|
+
<span className="grow">{l.label || l.url || 'Untitled'}{l.meta?.hidden ? ' · hidden' : ''}</span>
|
|
433
|
+
<b>{l.clicks.toLocaleString()}</b>
|
|
434
|
+
</div>
|
|
435
|
+
))}
|
|
436
|
+
</section>
|
|
437
|
+
|
|
438
|
+
{dirty && (
|
|
439
|
+
<div className="lm-savebar" role="status">
|
|
440
|
+
<span>{saveState === 'error' ? 'Save failed — try again' : 'Unsaved changes'}</span>
|
|
441
|
+
<button type="button" className="btn sm" onClick={discard} disabled={saveState === 'saving'}>Discard</button>
|
|
442
|
+
<button type="button" className="btn sm primary" onClick={() => void saveAll()} disabled={saveState === 'saving'}>
|
|
443
|
+
{saveState === 'saving' ? 'Saving…' : 'Save changes'}
|
|
444
|
+
</button>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
397
447
|
</div>
|
|
398
448
|
);
|
|
399
449
|
}
|
|
@@ -401,6 +451,7 @@ export default function LinksManager({ links: initial, settings, unsplashEnabled
|
|
|
401
451
|
// ---- rows -----------------------------------------------------------------
|
|
402
452
|
|
|
403
453
|
const KIND_BADGE: Partial<Record<Kind, string>> = {
|
|
454
|
+
live: '●',
|
|
404
455
|
heading: 'H', text: 'T', spacer: '↕', html: 'HTML', email: '@', phone: 'TEL', vcard: 'VCF',
|
|
405
456
|
};
|
|
406
457
|
|
|
@@ -434,7 +485,7 @@ function ItemRow({
|
|
|
434
485
|
return (
|
|
435
486
|
<div
|
|
436
487
|
ref={setNodeRef}
|
|
437
|
-
className={`lm-item ${isDragging ? 'is-dragging' : ''}`}
|
|
488
|
+
className={`lm-item ${isDragging ? 'is-dragging' : ''} ${link.meta?.hidden ? 'is-hidden' : ''}`}
|
|
438
489
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
|
439
490
|
>
|
|
440
491
|
<div className={`row lm-row ${hasEditorPane && editorOpen ? 'is-open' : ''}`}>
|
|
@@ -446,7 +497,7 @@ function ItemRow({
|
|
|
446
497
|
<span className="lm-badge">{KIND_BADGE[k]}</span>
|
|
447
498
|
)}
|
|
448
499
|
|
|
449
|
-
{k === 'spacer' ? (
|
|
500
|
+
{k === 'live' ? <span style={{ flex: 1 }}>Live video · <a href="/admin/live">Manage broadcast</a></span> : k === 'spacer' ? (
|
|
450
501
|
<>
|
|
451
502
|
<span className="bare strong" style={{ padding: '5px 8px' }}>Spacer</span>
|
|
452
503
|
<select
|
|
@@ -529,6 +580,16 @@ function ItemRow({
|
|
|
529
580
|
{editorOpen ? 'Done' : expandLabel}
|
|
530
581
|
</button>
|
|
531
582
|
)}
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
role="switch"
|
|
586
|
+
aria-checked={!link.meta?.hidden}
|
|
587
|
+
className={`lm-switch ${link.meta?.hidden ? '' : 'on'}`}
|
|
588
|
+
title={link.meta?.hidden ? 'Hidden on the page — click to show' : 'Visible — click to hide'}
|
|
589
|
+
onClick={() => onEdit(link.id, { meta: { ...link.meta, hidden: !link.meta?.hidden } }, true)}
|
|
590
|
+
>
|
|
591
|
+
<i />
|
|
592
|
+
</button>
|
|
532
593
|
<button type="button" className="btn sm ghost danger reveal" onClick={() => onDelete(link.id)}>Delete</button>
|
|
533
594
|
</div>
|
|
534
595
|
|