@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,265 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Admin from '../../../layouts/Admin.astro';
|
|
3
|
+
import Modal from '../../../components/Modal.astro';
|
|
4
|
+
import { getRepo } from '../../../lib/repo';
|
|
5
|
+
import { slugify, type CalendarVisibility } from '../../../lib/schema';
|
|
6
|
+
import { CALENDAR, calendarFeedUrl, calendarPageUrl } from '../../../lib/calendar/config';
|
|
7
|
+
import { safeColor } from '../../../lib/calendar/events';
|
|
8
|
+
import { normalizeCalendarSrc } from '../../../lib/calendar/src';
|
|
9
|
+
import { loadFeed } from '../../../lib/calendar/ics';
|
|
10
|
+
import { siteOrigin } from '../../../lib/seo';
|
|
11
|
+
|
|
12
|
+
// Calendars: create/edit native calendars and live feed subscriptions, set
|
|
13
|
+
// who can see each one, and copy the subscribe links people paste into
|
|
14
|
+
// Google Calendar, Apple Calendar or Outlook.
|
|
15
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
16
|
+
|
|
17
|
+
const repo = await getRepo(Astro.locals);
|
|
18
|
+
const settings = await repo.getSettings();
|
|
19
|
+
const origin = siteOrigin(settings, Astro.url);
|
|
20
|
+
const back = (notice?: string) => Astro.redirect(`/admin/calendar/calendars${notice ? `?notice=${encodeURIComponent(notice)}` : ''}`);
|
|
21
|
+
const VIS: CalendarVisibility[] = ['public', 'unlisted', 'private'];
|
|
22
|
+
const token = () => {
|
|
23
|
+
const bytes = crypto.getRandomValues(new Uint8Array(18));
|
|
24
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
25
|
+
};
|
|
26
|
+
async function freeSlug(raw: string, selfId?: string) {
|
|
27
|
+
const base = slugify(raw);
|
|
28
|
+
let slug = base;
|
|
29
|
+
for (let n = 2; ; n++) {
|
|
30
|
+
const hit = await repo.getCalendarBySlug(slug);
|
|
31
|
+
if (!hit || hit.id === selfId) return slug;
|
|
32
|
+
slug = `${base}-${n}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Astro.request.method === 'POST') {
|
|
37
|
+
const form = await Astro.request.formData();
|
|
38
|
+
// the edit modal's Delete button overrides the hidden op (it comes later
|
|
39
|
+
// in the form, so it's the last value)
|
|
40
|
+
const ops = form.getAll('op');
|
|
41
|
+
const op = String(ops[ops.length - 1]);
|
|
42
|
+
const id = String(form.get('id') || '');
|
|
43
|
+
const name = String(form.get('name') || '').trim().slice(0, 120);
|
|
44
|
+
const visibility = VIS.find((v) => v === form.get('visibility')) ?? 'public';
|
|
45
|
+
const color = safeColor(String(form.get('color') || '#4f46e5'));
|
|
46
|
+
const description = String(form.get('description') || '').slice(0, 1000);
|
|
47
|
+
|
|
48
|
+
if (op === 'create' || op === 'subscribe') {
|
|
49
|
+
if (!name) return back('Give the calendar a name.');
|
|
50
|
+
let feedUrl = '';
|
|
51
|
+
if (op === 'subscribe') {
|
|
52
|
+
feedUrl = normalizeCalendarSrc(String(form.get('feedUrl') || '')) ?? '';
|
|
53
|
+
if (!feedUrl) return back('Paste a Google Calendar link, a calendar ID, or an .ics feed URL.');
|
|
54
|
+
try { await loadFeed(feedUrl, Date.now()); } catch (err) { return back(err instanceof Error ? err.message : 'Couldn’t load that feed.'); }
|
|
55
|
+
}
|
|
56
|
+
await repo.createCalendar({ name, slug: await freeSlug(String(form.get('slug') || name)), color, description, visibility, kind: op === 'subscribe' ? 'feed' : 'native', feedUrl });
|
|
57
|
+
return back(`Created “${name}”.`);
|
|
58
|
+
}
|
|
59
|
+
const cal = id ? await repo.getCalendar(id) : null;
|
|
60
|
+
if (!cal) return back('That calendar no longer exists.');
|
|
61
|
+
if (op === 'update') {
|
|
62
|
+
let feedUrl = cal.feedUrl;
|
|
63
|
+
if (cal.kind === 'feed' && form.has('feedUrl') && String(form.get('feedUrl')) !== cal.feedUrl) {
|
|
64
|
+
feedUrl = normalizeCalendarSrc(String(form.get('feedUrl'))) ?? '';
|
|
65
|
+
if (!feedUrl) return back('Paste a Google Calendar link, a calendar ID, or an .ics feed URL.');
|
|
66
|
+
}
|
|
67
|
+
await repo.updateCalendar(cal.id, {
|
|
68
|
+
name: name || cal.name, slug: await freeSlug(String(form.get('slug') || name || cal.slug), cal.id),
|
|
69
|
+
color, description, visibility, feedUrl,
|
|
70
|
+
});
|
|
71
|
+
return back(`Saved “${name || cal.name}”.`);
|
|
72
|
+
}
|
|
73
|
+
if (op === 'rotate') {
|
|
74
|
+
await repo.updateCalendar(cal.id, { feedToken: token() });
|
|
75
|
+
return back('New private link created — the old one stopped working.');
|
|
76
|
+
}
|
|
77
|
+
if (op === 'move') {
|
|
78
|
+
const ids = (await repo.listCalendars()).map((c) => c.id);
|
|
79
|
+
const i = ids.indexOf(cal.id);
|
|
80
|
+
const j = form.get('dir') === 'up' ? i - 1 : i + 1;
|
|
81
|
+
if (j >= 0 && j < ids.length) [ids[i], ids[j]] = [ids[j], ids[i]];
|
|
82
|
+
await repo.reorderCalendars(ids);
|
|
83
|
+
return back();
|
|
84
|
+
}
|
|
85
|
+
if (op === 'delete') {
|
|
86
|
+
await repo.deleteCalendar(cal.id);
|
|
87
|
+
return back(`Deleted “${cal.name}”. Its events are still there, just unfiled.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [calendars, events] = await Promise.all([repo.listCalendars(), repo.listEvents()]);
|
|
92
|
+
const counts = new Map<string, number>();
|
|
93
|
+
for (const e of events) for (const id of e.calendarIds) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
94
|
+
const notice = Astro.url.searchParams.get('notice') ?? '';
|
|
95
|
+
const feedFor = (c: (typeof calendars)[number]) =>
|
|
96
|
+
origin + calendarFeedUrl(c.slug) + (c.visibility === 'private' ? `?t=${encodeURIComponent(c.feedToken)}` : '');
|
|
97
|
+
const hostOf = (u: string) => { try { return new URL(u).hostname.replace(/^www\./, ''); } catch { return u; } };
|
|
98
|
+
const VIS_LABEL: Record<CalendarVisibility, string> = {
|
|
99
|
+
public: 'Public — listed on the calendar page',
|
|
100
|
+
unlisted: 'Unlisted — event pages and feed work, not listed',
|
|
101
|
+
private: 'Private — admins and the secret feed link only',
|
|
102
|
+
};
|
|
103
|
+
const I = {
|
|
104
|
+
up: 'M6 15l6-6 6 6', down: 'M6 9l6 6 6-6', copy: 'M9 9h10v10H9zM5 15V5h10',
|
|
105
|
+
ext: 'M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5',
|
|
106
|
+
};
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
<Admin title="Calendars" section="calendar:calendars">
|
|
110
|
+
<div class="page-head">
|
|
111
|
+
<div>
|
|
112
|
+
<h1>Calendars</h1>
|
|
113
|
+
<p class="sub">Group events, choose who sees them, and share subscribe links for Google, Apple and Outlook.</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="page-actions">
|
|
116
|
+
<button class="btn" type="button" data-modal="cal-subscribe">Subscribe to a feed</button>
|
|
117
|
+
<button class="btn primary" type="button" data-modal="cal-new">New calendar</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{notice && <p class="notice" role="status">{notice}</p>}
|
|
122
|
+
|
|
123
|
+
<section class="card cal-allfeed">
|
|
124
|
+
<div>
|
|
125
|
+
<b>Everything public</b>
|
|
126
|
+
<small>One link with every public calendar — paste it into any calendar app.</small>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="cal-feed">
|
|
129
|
+
<input type="text" class="mono" readonly value={origin + calendarFeedUrl()} aria-label="Subscribe link for all public calendars" />
|
|
130
|
+
<button class="btn sm" type="button" data-copy={origin + calendarFeedUrl()}>Copy</button>
|
|
131
|
+
</div>
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
{calendars.length === 0 ? (
|
|
135
|
+
<div class="empty-state">
|
|
136
|
+
<b>No calendars yet</b>
|
|
137
|
+
<p>Events work without calendars. Add some to color-code events, filter the calendar page, and give each group its own subscribe link.</p>
|
|
138
|
+
<div class="empty-actions">
|
|
139
|
+
<button class="btn" type="button" data-modal="cal-subscribe">Subscribe to a feed</button>
|
|
140
|
+
<button class="btn primary" type="button" data-modal="cal-new">New calendar</button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div class="cal-list">
|
|
145
|
+
{calendars.map((c, i) => (
|
|
146
|
+
<article class="cal-row" id={`cal-${c.id}`}>
|
|
147
|
+
<i class="cal-dot lg" style={`background:${safeColor(c.color)}`} aria-hidden="true" />
|
|
148
|
+
<div class="cal-main">
|
|
149
|
+
<div class="cal-title">
|
|
150
|
+
<b>{c.name}</b>
|
|
151
|
+
{c.kind === 'feed' && <span class="pill lang">Feed · {hostOf(c.feedUrl)}</span>}
|
|
152
|
+
<span class={`cal-vis is-${c.visibility}`}>{c.visibility[0].toUpperCase() + c.visibility.slice(1)}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<small>
|
|
155
|
+
{c.kind === 'feed' ? 'Live mirror, refreshed every few minutes · read-only' : `${counts.get(c.id) ?? 0} event${counts.get(c.id) === 1 ? '' : 's'}`}
|
|
156
|
+
{c.description && ` · ${c.description}`}
|
|
157
|
+
</small>
|
|
158
|
+
{c.visibility !== 'private' || c.feedToken ? (
|
|
159
|
+
<div class="cal-feed">
|
|
160
|
+
<input type="text" class="mono" readonly value={feedFor(c)} aria-label={`Subscribe link for ${c.name}`} />
|
|
161
|
+
<button class="btn sm" type="button" data-copy={feedFor(c)}>Copy</button>
|
|
162
|
+
{c.visibility === 'private' && (
|
|
163
|
+
<form method="post">
|
|
164
|
+
<input type="hidden" name="op" value="rotate" /><input type="hidden" name="id" value={c.id} />
|
|
165
|
+
<button class="btn sm ghost" type="submit" data-confirm="Make a new private link? Anyone subscribed with the old one stops getting updates.">New link</button>
|
|
166
|
+
</form>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
) : null}
|
|
170
|
+
</div>
|
|
171
|
+
<div class="cal-actions">
|
|
172
|
+
{c.visibility !== 'private' && (
|
|
173
|
+
<a class="icon-btn" href={calendarPageUrl(c.slug)} target="_blank" rel="noopener" title="View on site" aria-label="View on site">
|
|
174
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={I.ext} /></svg>
|
|
175
|
+
</a>
|
|
176
|
+
)}
|
|
177
|
+
<form method="post" class="cal-move">
|
|
178
|
+
<input type="hidden" name="op" value="move" /><input type="hidden" name="id" value={c.id} />
|
|
179
|
+
<button class="icon-btn" name="dir" value="up" disabled={i === 0} title="Move up" aria-label="Move up"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d={I.up} /></svg></button>
|
|
180
|
+
<button class="icon-btn" name="dir" value="down" disabled={i === calendars.length - 1} title="Move down" aria-label="Move down"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d={I.down} /></svg></button>
|
|
181
|
+
</form>
|
|
182
|
+
<button class="btn sm" type="button" data-modal={`cal-edit-${c.id}`}>Edit</button>
|
|
183
|
+
</div>
|
|
184
|
+
<Modal id={`cal-edit-${c.id}`} title={`Edit ${c.name}`}>
|
|
185
|
+
<form method="post">
|
|
186
|
+
<input type="hidden" name="op" value="update" /><input type="hidden" name="id" value={c.id} />
|
|
187
|
+
<div class="field-row">
|
|
188
|
+
<label class="field"><span>Name</span><input type="text" name="name" value={c.name} required /></label>
|
|
189
|
+
<label class="field"><span>Color</span><input type="color" name="color" value={safeColor(c.color)} /></label>
|
|
190
|
+
</div>
|
|
191
|
+
<label class="field"><span>URL name</span><input class="mono" type="text" name="slug" value={c.slug} /></label>
|
|
192
|
+
{c.kind === 'feed' && <label class="field"><span>Feed link</span><input class="mono" type="text" name="feedUrl" value={c.feedUrl} /></label>}
|
|
193
|
+
<label class="field"><span>Description</span><input type="text" name="description" value={c.description} placeholder="Shown on the calendar page filter" /></label>
|
|
194
|
+
<label class="field">
|
|
195
|
+
<span>Who can see it</span>
|
|
196
|
+
<select name="visibility">
|
|
197
|
+
{VIS.map((v) => <option value={v} selected={c.visibility === v}>{VIS_LABEL[v]}</option>)}
|
|
198
|
+
</select>
|
|
199
|
+
</label>
|
|
200
|
+
<div class="modal-foot">
|
|
201
|
+
<button class="btn danger ghost" type="submit" name="op" value="delete" formnovalidate data-confirm={`Delete “${c.name}”? Events stay, they just lose this calendar.`}>Delete</button>
|
|
202
|
+
<span class="spacer" />
|
|
203
|
+
<button class="btn ghost" type="button" data-close>Cancel</button>
|
|
204
|
+
<button class="btn primary" type="submit">Save</button>
|
|
205
|
+
</div>
|
|
206
|
+
</form>
|
|
207
|
+
</Modal>
|
|
208
|
+
</article>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
<Modal id="cal-new" title="New calendar">
|
|
214
|
+
<form method="post">
|
|
215
|
+
<input type="hidden" name="op" value="create" />
|
|
216
|
+
<div class="field-row">
|
|
217
|
+
<label class="field"><span>Name</span><input type="text" name="name" placeholder="Youth" required autofocus /></label>
|
|
218
|
+
<label class="field"><span>Color</span><input type="color" name="color" value="#4f46e5" /></label>
|
|
219
|
+
</div>
|
|
220
|
+
<label class="field"><span>Description</span><input type="text" name="description" placeholder="Optional" /></label>
|
|
221
|
+
<label class="field">
|
|
222
|
+
<span>Who can see it</span>
|
|
223
|
+
<select name="visibility">{VIS.map((v) => <option value={v}>{VIS_LABEL[v]}</option>)}</select>
|
|
224
|
+
</label>
|
|
225
|
+
<div class="modal-foot">
|
|
226
|
+
<button class="btn ghost" type="button" data-close>Cancel</button>
|
|
227
|
+
<button class="btn primary" type="submit">Create calendar</button>
|
|
228
|
+
</div>
|
|
229
|
+
</form>
|
|
230
|
+
</Modal>
|
|
231
|
+
|
|
232
|
+
<Modal id="cal-subscribe" title="Subscribe to a feed">
|
|
233
|
+
<form method="post">
|
|
234
|
+
<input type="hidden" name="op" value="subscribe" />
|
|
235
|
+
<p class="hint">Mirror a Google, Apple or Outlook calendar. Its events show up read-only and stay in sync.</p>
|
|
236
|
+
<div class="field-row">
|
|
237
|
+
<label class="field"><span>Name</span><input type="text" name="name" placeholder="Staff calendar" required /></label>
|
|
238
|
+
<label class="field"><span>Color</span><input type="color" name="color" value="#2f9e6e" /></label>
|
|
239
|
+
</div>
|
|
240
|
+
<label class="field"><span>Calendar link</span><input class="mono" type="text" name="feedUrl" placeholder="Google Calendar link, calendar ID, or .ics URL" required /></label>
|
|
241
|
+
<label class="field">
|
|
242
|
+
<span>Who can see it</span>
|
|
243
|
+
<select name="visibility">{VIS.map((v) => <option value={v}>{VIS_LABEL[v]}</option>)}</select>
|
|
244
|
+
</label>
|
|
245
|
+
<div class="modal-foot">
|
|
246
|
+
<button class="btn ghost" type="button" data-close>Cancel</button>
|
|
247
|
+
<button class="btn primary" type="submit">Subscribe</button>
|
|
248
|
+
</div>
|
|
249
|
+
</form>
|
|
250
|
+
</Modal>
|
|
251
|
+
</Admin>
|
|
252
|
+
|
|
253
|
+
<script>
|
|
254
|
+
document.addEventListener('click', async (e) => {
|
|
255
|
+
const b = e.target instanceof Element ? e.target.closest<HTMLButtonElement>('[data-copy]') : null;
|
|
256
|
+
if (b) {
|
|
257
|
+
await navigator.clipboard.writeText(b.dataset.copy ?? '');
|
|
258
|
+
const label = b.textContent;
|
|
259
|
+
b.textContent = 'Copied';
|
|
260
|
+
setTimeout(() => (b.textContent = label), 1200);
|
|
261
|
+
}
|
|
262
|
+
const c = e.target instanceof Element ? e.target.closest<HTMLElement>('[data-confirm]') : null;
|
|
263
|
+
if (c && !confirm(c.dataset.confirm)) e.preventDefault();
|
|
264
|
+
});
|
|
265
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRepo } from '../../../../lib/repo';
|
|
3
|
+
import { createPreviewToken } from '../../../../lib/preview';
|
|
4
|
+
import { env } from '../../../../lib/env';
|
|
5
|
+
import { BRAND } from '../../../../lib/brand';
|
|
6
|
+
import { CALENDAR, eventUrl } from '../../../../lib/calendar/config';
|
|
7
|
+
import { mergeEventDraft } from '../../../../lib/drafts';
|
|
8
|
+
import { safeColor } from '../../../../lib/calendar/events';
|
|
9
|
+
import ThemeScript from '../../../../components/ThemeScript.astro';
|
|
10
|
+
import PostEditor from '../../../../components/editor/PostEditor';
|
|
11
|
+
import '../../../../styles/admin.css';
|
|
12
|
+
import '../../../../styles/editor.css';
|
|
13
|
+
|
|
14
|
+
// The shared writing surface in event mode: same cards, autosave, preview and
|
|
15
|
+
// publish flow as posts, plus the When / Repeats / Where / Button / Calendars
|
|
16
|
+
// sidebar. Live events stage edits until Update, like posts.
|
|
17
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
18
|
+
|
|
19
|
+
const repo = await getRepo(Astro.locals);
|
|
20
|
+
const event = await repo.getEvent(Astro.params.id!);
|
|
21
|
+
if (!event) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
22
|
+
|
|
23
|
+
const editable = mergeEventDraft(event);
|
|
24
|
+
const [settings, calendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
|
|
25
|
+
const previewPath = `/preview/event/${event.id}`;
|
|
26
|
+
const sharePath = `${previewPath}?token=${createPreviewToken('event', event.id)}`;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<!doctype html>
|
|
30
|
+
<html lang="en">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="utf-8" />
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-visual" />
|
|
34
|
+
<ThemeScript />
|
|
35
|
+
<meta name="robots" content="noindex" />
|
|
36
|
+
<title>{event.title || 'Untitled event'} · {BRAND.name} editor</title>
|
|
37
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
38
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<PostEditor
|
|
42
|
+
client:only="react"
|
|
43
|
+
kind="event"
|
|
44
|
+
event={editable}
|
|
45
|
+
initialHTML={editable.bodyHtml}
|
|
46
|
+
unsplashEnabled={Boolean(env('UNSPLASH_ACCESS_KEY'))}
|
|
47
|
+
previewPath={previewPath}
|
|
48
|
+
sharePath={sharePath}
|
|
49
|
+
livePath={eventUrl(event.slug)}
|
|
50
|
+
calendars={calendars.filter((c) => c.kind === 'native').map((c) => ({ id: c.id, name: c.name, color: safeColor(c.color) }))}
|
|
51
|
+
eventFields={CALENDAR.eventFields}
|
|
52
|
+
siteTimezone={settings.timezone}
|
|
53
|
+
site={{ title: settings.siteTitle, url: settings.siteUrl }}
|
|
54
|
+
/>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|