@acms-subir/core 0.3.3-next.20260929113209 → 0.3.4
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 +105 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/preview.mjs +8 -4
- package/cli/sections.mjs +188 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +19 -3
- package/integration.mjs +76 -5
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +17 -13
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +205 -174
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/booking/BookingFlow.astro +6 -3
- package/src/components/calendar/BookingsHead.astro +51 -0
- package/src/components/calendar/CalendarBoard.tsx +618 -0
- package/src/components/calendar/MiniMonth.tsx +54 -0
- package/src/components/calendar/NewEventDialog.tsx +44 -0
- package/src/components/calendar/QuickAdd.tsx +263 -0
- package/src/components/calendar/RoomHolds.tsx +203 -0
- package/src/components/calendar/WhereItLands.tsx +51 -0
- package/src/components/editor/EventPanel.tsx +23 -2
- package/src/components/editor/PostEditor.tsx +206 -14
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +11 -7
- package/src/layouts/Site.astro +136 -21
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/backup/content.ts +6 -1
- package/src/lib/booking/agent.ts +3 -2
- package/src/lib/booking/approval.ts +70 -0
- package/src/lib/booking/holds.ts +120 -0
- package/src/lib/booking/service.ts +37 -3
- package/src/lib/calendar/agent-schema.ts +9 -1
- package/src/lib/calendar/agent.ts +16 -1
- package/src/lib/calendar/board.ts +171 -0
- package/src/lib/calendar/config.ts +3 -0
- package/src/lib/calendar/grid.ts +169 -0
- package/src/lib/calendar/migration.ts +16 -0
- package/src/lib/calendar/render.ts +9 -7
- package/src/lib/calendar/schedule.ts +166 -0
- package/src/lib/content-service.ts +203 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +26 -14
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +14 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +14 -0
- package/src/lib/repo/sql.ts +65 -20
- package/src/lib/repo/types.ts +10 -4
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +6 -1
- package/src/lib/schema.ts +76 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +242 -0
- package/src/lib/site-chrome.ts +34 -0
- package/src/lib/theme-assets.ts +16 -0
- package/src/lib/theme.ts +33 -4
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +38 -2
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/bookings.astro +32 -26
- package/src/pages/admin/calendar/calendars.astro +4 -263
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/calendar/events/index.astro +41 -83
- package/src/pages/admin/calendar/index.astro +43 -0
- package/src/pages/admin/calendar/resources/[id].astro +27 -5
- package/src/pages/admin/calendar/resources/index.astro +16 -17
- package/src/pages/admin/calendar/resources/link.astro +52 -12
- package/src/pages/admin/calendar/settings.astro +458 -0
- package/src/pages/admin/integrations.astro +5 -11
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/book/[resource].ics.ts +7 -1
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +117 -0
- package/src/styles/cards.css +5 -1
- package/src/styles/editor.css +57 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/schedule.css +325 -0
- package/src/styles/sections.css +21 -12
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +330 -0
- package/src/theme-sdk/index.ts +14 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +4 -0
- package/src/pages/admin/calendar/index.ts +0 -4
|
@@ -1,265 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
};
|
|
2
|
+
// The Calendars page moved into Calendar settings (⚙ on Schedule).
|
|
3
|
+
// Kept as a redirect so bookmarks, emails and old links keep working.
|
|
4
|
+
const hash = Astro.url.searchParams.has('subscribe') ? '?subscribe=1#subscribed' : '#calendars';
|
|
5
|
+
return Astro.redirect(`/admin/calendar/settings${hash}`, 301);
|
|
107
6
|
---
|
|
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>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { I18N, extraLocales, localeName } from '../../../../lib/i18n';
|
|
3
|
+
import { translationEngine } from '../../../../lib/translate';
|
|
2
4
|
import { getRepo } from '../../../../lib/repo';
|
|
3
5
|
import { createPreviewToken } from '../../../../lib/preview';
|
|
4
6
|
import { env } from '../../../../lib/env';
|
|
@@ -24,6 +26,10 @@ const editable = mergeEventDraft(event);
|
|
|
24
26
|
const [settings, calendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
|
|
25
27
|
const previewPath = `/preview/event/${event.id}`;
|
|
26
28
|
const sharePath = `${previewPath}?token=${createPreviewToken('event', event.id)}`;
|
|
29
|
+
// other languages this document can be written in (acms({ i18n }))
|
|
30
|
+
const trLanguages = extraLocales().map((code) => ({ code, name: localeName(code, true) }));
|
|
31
|
+
const trMain = { code: I18N.defaultLocale, name: localeName(I18N.defaultLocale || 'en', true) };
|
|
32
|
+
const trDrafts = trLanguages.length > 0 && translationEngine((await repo.getSettings()).ai, Astro.locals) !== null;
|
|
27
33
|
---
|
|
28
34
|
|
|
29
35
|
<!doctype html>
|
|
@@ -42,6 +48,9 @@ const sharePath = `${previewPath}?token=${createPreviewToken('event', event.id)}
|
|
|
42
48
|
client:only="react"
|
|
43
49
|
kind="event"
|
|
44
50
|
event={editable}
|
|
51
|
+
languages={trLanguages}
|
|
52
|
+
mainLanguage={trMain}
|
|
53
|
+
translationDrafts={trDrafts}
|
|
45
54
|
initialHTML={editable.bodyHtml}
|
|
46
55
|
unsplashEnabled={Boolean(env('UNSPLASH_ACCESS_KEY'))}
|
|
47
56
|
previewPath={previewPath}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Admin from '../../../../layouts/Admin.astro';
|
|
3
3
|
import Modal from '../../../../components/Modal.astro';
|
|
4
|
+
import NewEventDialog from '../../../../components/calendar/NewEventDialog';
|
|
5
|
+
import { bookingsEnabled } from '../../../../lib/calendar/config';
|
|
6
|
+
import { todayIn } from '../../../../lib/calendar/board';
|
|
4
7
|
import ContentList, { type ContentItem } from '../../../../components/ContentList.astro';
|
|
5
8
|
import { getRepo } from '../../../../lib/repo';
|
|
6
9
|
import { slugify, type Status } from '../../../../lib/schema';
|
|
@@ -16,9 +19,10 @@ import { siteLocale } from '../../../../lib/dates';
|
|
|
16
19
|
import { waitUntilFrom } from '../../../../lib/webhooks';
|
|
17
20
|
import { invalidateRedirectCache } from '../../../../middleware';
|
|
18
21
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// action
|
|
22
|
+
// Schedule → List: upcoming first, grouped by the month each event next
|
|
23
|
+
// happens. Subscribed calendars appear as read-only rows with a "Make an
|
|
24
|
+
// editable copy" action. New events open the quick-add dialog (with its mini
|
|
25
|
+
// month); subscribing lives in Calendar settings.
|
|
22
26
|
if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
23
27
|
|
|
24
28
|
const repo = await getRepo(Astro.locals);
|
|
@@ -107,23 +111,6 @@ if (Astro.request.method === 'POST') {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
// live mirror of an external calendar
|
|
111
|
-
if (op === 'subscribe') {
|
|
112
|
-
const url = normalizeCalendarSrc(String(form.get('url') || ''));
|
|
113
|
-
const name = String(form.get('name') || '').trim() || 'Imported calendar';
|
|
114
|
-
if (!url) return back('Paste a Google Calendar link, a calendar ID, or an .ics feed URL.');
|
|
115
|
-
try {
|
|
116
|
-
await loadFeed(url, now);
|
|
117
|
-
} catch (err) {
|
|
118
|
-
return back(err instanceof Error ? err.message : 'Couldn’t load that feed.');
|
|
119
|
-
}
|
|
120
|
-
const base = slugify(name);
|
|
121
|
-
let slug = base;
|
|
122
|
-
for (let n = 2; await repo.getCalendarBySlug(slug); n++) slug = `${base}-${n}`;
|
|
123
|
-
await repo.createCalendar({ name, slug, kind: 'feed', feedUrl: url, color: safeColor(String(form.get('color') || '#2f9e6e')) });
|
|
124
|
-
return back(`Subscribed to “${name}” — its events now show on your calendar.`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
114
|
// one-time import: .ics / .csv file, or an ICS URL, into drafts
|
|
128
115
|
if (op === 'import') {
|
|
129
116
|
const calendarId = String(form.get('calendarId') || '');
|
|
@@ -221,16 +208,16 @@ if ((scope === 'upcoming' || scope === 'all') && feedCals.length) {
|
|
|
221
208
|
const uid = uids.get(`${occ.title}|${occ.location}`) ?? '';
|
|
222
209
|
items.push({
|
|
223
210
|
id: `feed-${c.id}-${occ.start}`,
|
|
224
|
-
href: `/admin/calendar/
|
|
211
|
+
href: `/admin/calendar/settings#cal-${c.id}`,
|
|
225
212
|
title: occ.title,
|
|
226
213
|
sub: '',
|
|
227
214
|
meta: [occ.location, `from ${c.name}`].filter(Boolean).join(' · '),
|
|
228
215
|
when: occ.allDay ? f.range : f.range,
|
|
229
216
|
at: occ.start,
|
|
230
217
|
status: 'feed',
|
|
231
|
-
statusLabel: '
|
|
218
|
+
statusLabel: 'Subscribed · read-only',
|
|
232
219
|
readonly: true,
|
|
233
|
-
action: uid ? { label: '
|
|
220
|
+
action: uid ? { label: 'Make editable copy', fields: { op: 'copy', calendarId: c.id, uid } } : undefined,
|
|
234
221
|
});
|
|
235
222
|
}
|
|
236
223
|
} catch (err) {
|
|
@@ -261,72 +248,45 @@ const tomorrowIso = `${tomorrow.y}-${String(tomorrow.m).padStart(2, '0')}-${Stri
|
|
|
261
248
|
const subBits = [
|
|
262
249
|
`${counts.upcoming} upcoming`,
|
|
263
250
|
counts.drafts ? `${counts.drafts} draft${counts.drafts === 1 ? '' : 's'}` : '',
|
|
264
|
-
feedCals.length ? `${feedCals.length}
|
|
251
|
+
feedCals.length ? `${feedCals.length} subscribed calendar${feedCals.length === 1 ? '' : 's'}` : '',
|
|
265
252
|
].filter(Boolean);
|
|
266
253
|
---
|
|
267
254
|
|
|
268
|
-
<Admin title="
|
|
255
|
+
<Admin title="Schedule" section="calendar:schedule">
|
|
269
256
|
<div class="page-head">
|
|
270
257
|
<div>
|
|
271
|
-
<h1>
|
|
258
|
+
<h1>All events</h1>
|
|
272
259
|
<p class="sub">{subBits.join(' · ')} · public at <a href={CALENDAR.base} target="_blank" rel="noopener">{CALENDAR.base}</a></p>
|
|
273
260
|
</div>
|
|
274
261
|
<div class="page-actions">
|
|
275
|
-
<
|
|
276
|
-
|
|
262
|
+
<nav class="seg" aria-label="View">
|
|
263
|
+
<a href="/admin/calendar">Month</a>
|
|
264
|
+
<a href="/admin/calendar?view=week">Week</a>
|
|
265
|
+
<a class="on" href="/admin/calendar/events" aria-current="page">List</a>
|
|
266
|
+
</nav>
|
|
267
|
+
<a class="btn ico" href="/admin/calendar/settings" title="Calendar settings" aria-label="Calendar settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg></a>
|
|
268
|
+
<details class="newmenu">
|
|
269
|
+
<summary class="btn primary">New <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d={chev} /></svg></summary>
|
|
270
|
+
<div class="newmenu-pop" role="menu">
|
|
271
|
+
<button type="button" role="menuitem" data-new-event>Event <kbd>N</kbd></button>
|
|
272
|
+
{bookingsEnabled() && <a role="menuitem" href="/admin/calendar/bookings?block=1">Block time <small>room closed, time off</small></a>}
|
|
273
|
+
<button type="button" role="menuitem" data-modal="import-events">Import events… <small>.ics or .csv</small></button>
|
|
274
|
+
</div>
|
|
275
|
+
</details>
|
|
277
276
|
</div>
|
|
278
277
|
</div>
|
|
279
278
|
|
|
280
279
|
{notice && <p class="notice" role="status">{notice}</p>}
|
|
281
280
|
{feedErrors.map((e) => <p class="notice warn">Couldn’t refresh {e}</p>)}
|
|
282
281
|
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<label class="field">
|
|
287
|
-
<span>Title</span>
|
|
288
|
-
<input type="text" name="title" placeholder="Fall Festival" required autofocus />
|
|
289
|
-
</label>
|
|
290
|
-
<div class="field-row">
|
|
291
|
-
<label class="field"><span>Date</span><input type="date" name="date" value={tomorrowIso} required /></label>
|
|
292
|
-
<label class="field"><span>Time <em class="hint">blank = all day</em></span><input type="time" name="time" value="10:00" step="300" /></label>
|
|
293
|
-
</div>
|
|
294
|
-
{nativeCals.length > 0 && (
|
|
295
|
-
<label class="field">
|
|
296
|
-
<span>Calendar</span>
|
|
297
|
-
<select name="calendarId">
|
|
298
|
-
<option value="">None — main calendar only</option>
|
|
299
|
-
{nativeCals.map((c) => <option value={c.id} selected={c.id === calFilter}>{c.name}</option>)}
|
|
300
|
-
</select>
|
|
301
|
-
</label>
|
|
302
|
-
)}
|
|
303
|
-
<div class="modal-foot">
|
|
304
|
-
<button class="btn ghost" type="button" data-close>Cancel</button>
|
|
305
|
-
<button class="btn primary" type="submit">Create event</button>
|
|
306
|
-
</div>
|
|
307
|
-
<p class="hint modal-note">Creates a draft and opens the editor, where you add the image, location, button and repeat rule.</p>
|
|
308
|
-
</form>
|
|
309
|
-
</Modal>
|
|
282
|
+
<NewEventDialog client:load calendars={nativeCals.map((c) => ({ id: c.id, name: c.name, color: safeColor(c.color) }))}
|
|
283
|
+
defaultCalendarId={settings.calendar.defaultCalendarId || calFilter} weekStart={settings.calendar.weekStart} locale={locale}
|
|
284
|
+
today={todayIn(tz)} day={tomorrowIso} open={Astro.url.searchParams.has('new')} />
|
|
310
285
|
|
|
311
|
-
<Modal id="import-events" title="
|
|
312
|
-
<
|
|
313
|
-
<button type="button" role="tab" aria-selected="true" data-tab="subscribe">Subscribe to a calendar</button>
|
|
314
|
-
<button type="button" role="tab" aria-selected="false" data-tab="once">Import once</button>
|
|
315
|
-
</div>
|
|
316
|
-
<form method="post" data-panel="subscribe">
|
|
317
|
-
<input type="hidden" name="op" value="subscribe" />
|
|
318
|
-
<p class="hint">Mirror a Google, Apple or Outlook calendar live. Its events show on your calendar and stay in sync, read-only.</p>
|
|
319
|
-
<label class="field"><span>Name</span><input type="text" name="name" placeholder="Staff calendar" required /></label>
|
|
320
|
-
<label class="field"><span>Calendar link</span><input class="mono" type="text" name="url" placeholder="Google Calendar link, calendar ID, or an .ics URL" required /></label>
|
|
321
|
-
<label class="field"><span>Color</span><input type="color" name="color" value="#2f9e6e" /></label>
|
|
322
|
-
<div class="modal-foot">
|
|
323
|
-
<button class="btn ghost" type="button" data-close>Cancel</button>
|
|
324
|
-
<button class="btn primary" type="submit">Subscribe</button>
|
|
325
|
-
</div>
|
|
326
|
-
</form>
|
|
327
|
-
<form method="post" enctype="multipart/form-data" data-panel="once" hidden>
|
|
286
|
+
<Modal id="import-events" title="Import events" open={Astro.url.searchParams.has('import')}>
|
|
287
|
+
<form method="post" enctype="multipart/form-data">
|
|
328
288
|
<input type="hidden" name="op" value="import" />
|
|
329
|
-
<p class="hint">Copy events in as editable drafts. Works with .ics exports from Google, Apple and Outlook, or a spreadsheet saved as .csv with Title and Start date columns
|
|
289
|
+
<p class="hint">Copy events in once, as editable drafts. Works with .ics exports from Google, Apple and Outlook, or a spreadsheet saved as .csv with Title and Start date columns. To keep a calendar in sync instead, <a href="/admin/calendar/settings?subscribe=1#subscribed">subscribe to it</a>.</p>
|
|
330
290
|
<label class="field"><span>File</span><input type="file" name="file" accept=".ics,.csv,text/calendar,text/csv" /></label>
|
|
331
291
|
<label class="field"><span>Or a calendar link</span><input class="mono" type="text" name="url" placeholder="https://calendar.google.com/…" /></label>
|
|
332
292
|
{nativeCals.length > 0 && (
|
|
@@ -348,10 +308,9 @@ const subBits = [
|
|
|
348
308
|
{events.length === 0 && feedCals.length === 0 ? (
|
|
349
309
|
<div class="empty-state">
|
|
350
310
|
<b>No events yet</b>
|
|
351
|
-
<p>
|
|
311
|
+
<p>Events are things that happen on a date. Add one, or import a calendar file from Google, Apple or Outlook.</p>
|
|
352
312
|
<div class="empty-actions">
|
|
353
|
-
<button class="btn" type="button" data-
|
|
354
|
-
<button class="btn primary" type="button" data-modal="new-event">New event</button>
|
|
313
|
+
<button class="btn primary" type="button" data-new-event>New event</button>
|
|
355
314
|
</div>
|
|
356
315
|
</div>
|
|
357
316
|
) : (
|
|
@@ -386,7 +345,7 @@ const subBits = [
|
|
|
386
345
|
<ContentList items={items} noun="event" timeZone={settings.timezone || undefined} groupByMonth={true} empty="No events match the current filters." />
|
|
387
346
|
{items.length === 0 && (
|
|
388
347
|
<p class="hint" style="padding:24px 8px">
|
|
389
|
-
{scope === 'upcoming' ? 'Nothing coming up.' : 'No events match the current filters.'} <
|
|
348
|
+
{scope === 'upcoming' ? 'Nothing coming up.' : 'No events match the current filters.'} <button class="linklike" type="button" data-new-event>Add an event</button>
|
|
390
349
|
</p>
|
|
391
350
|
)}
|
|
392
351
|
</>
|
|
@@ -399,10 +358,9 @@ const subBits = [
|
|
|
399
358
|
if (!(e.target instanceof Node && d.contains(e.target))) d.open = false;
|
|
400
359
|
});
|
|
401
360
|
});
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}));
|
|
361
|
+
// the New ▾ menu closes like the filter menus
|
|
362
|
+
document.addEventListener('click', (e) => {
|
|
363
|
+
const m = document.querySelector<HTMLDetailsElement>('details.newmenu[open]');
|
|
364
|
+
if (m && (!(e.target instanceof Node) || !m.contains(e.target) || (e.target as Element).closest('[role=menuitem]'))) m.open = false;
|
|
365
|
+
});
|
|
408
366
|
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Admin from '../../../layouts/Admin.astro';
|
|
3
|
+
import CalendarBoard from '../../../components/calendar/CalendarBoard';
|
|
4
|
+
import { getRepo } from '../../../lib/repo';
|
|
5
|
+
import { CALENDAR, bookingsEnabled } from '../../../lib/calendar/config';
|
|
6
|
+
import { loadBoard, todayIn } from '../../../lib/calendar/board';
|
|
7
|
+
import { isIsoDay, isIsoMonth, monthWeeks, weekDays } from '../../../lib/calendar/grid';
|
|
8
|
+
import { siteLocale } from '../../../lib/dates';
|
|
9
|
+
|
|
10
|
+
// Schedule: the Calendar section's home — a month (or week) grid of every
|
|
11
|
+
// event, draft, subscribed calendar and booking. ?m=YYYY-MM picks the month,
|
|
12
|
+
// ?view=week&d=YYYY-MM-DD a week; the list lives at /admin/calendar/events.
|
|
13
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
|
|
14
|
+
if (Astro.url.searchParams.get('view') === 'list') return Astro.redirect('/admin/calendar/events');
|
|
15
|
+
|
|
16
|
+
const repo = await getRepo(Astro.locals);
|
|
17
|
+
const settings = await repo.getSettings();
|
|
18
|
+
const tz = settings.timezone || 'UTC';
|
|
19
|
+
const today = todayIn(tz);
|
|
20
|
+
const weekStart = settings.calendar.weekStart;
|
|
21
|
+
const view = Astro.url.searchParams.get('view') === 'week' ? 'week' : 'month';
|
|
22
|
+
const d = Astro.url.searchParams.get('d') ?? '';
|
|
23
|
+
const m = Astro.url.searchParams.get('m') ?? '';
|
|
24
|
+
const anchor = isIsoDay(d) ? d : isIsoMonth(m) ? `${m}-01` : today;
|
|
25
|
+
const weeks = monthWeeks(anchor.slice(0, 7), weekStart);
|
|
26
|
+
const [from, to] = view === 'week' ? [weekDays(anchor, weekStart)[0], weekDays(anchor, weekStart)[6]] : [weeks[0][0], weeks[weeks.length - 1][6]];
|
|
27
|
+
const initial = await loadBoard(repo, settings, from, to);
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<Admin title="Schedule" section="calendar:schedule" wide>
|
|
31
|
+
<CalendarBoard
|
|
32
|
+
client:load
|
|
33
|
+
initial={initial}
|
|
34
|
+
view={view}
|
|
35
|
+
anchor={anchor}
|
|
36
|
+
today={today}
|
|
37
|
+
weekStart={weekStart}
|
|
38
|
+
locale={siteLocale(settings)}
|
|
39
|
+
defaultCalendarId={settings.calendar.defaultCalendarId}
|
|
40
|
+
bookings={bookingsEnabled()}
|
|
41
|
+
listHref="/admin/calendar/events"
|
|
42
|
+
/>
|
|
43
|
+
</Admin>
|
|
@@ -9,6 +9,9 @@ import { describeLink } from '../../../../lib/booking/availability';
|
|
|
9
9
|
import { isValidTimeZone } from '../../../../lib/calendar/events';
|
|
10
10
|
import { normalizeCalendarSrc } from '../../../../lib/calendar/src';
|
|
11
11
|
import { siteOrigin } from '../../../../lib/seo';
|
|
12
|
+
import { approvalShort, linkApproval } from '../../../../lib/booking/approval';
|
|
13
|
+
import { eventHolds } from '../../../../lib/booking/holds';
|
|
14
|
+
import { fmt } from '../../../../lib/booking/view';
|
|
12
15
|
|
|
13
16
|
// One resource: details, weekly hours, date overrides, calendars that block
|
|
14
17
|
// time, who gets notified, the staff feed, and its booking links.
|
|
@@ -53,7 +56,7 @@ if (Astro.request.method === 'POST') {
|
|
|
53
56
|
const base = slugify(name);
|
|
54
57
|
let slug = base;
|
|
55
58
|
for (let n = 2; await repo.getBookingLinkBySlug(id, slug); n++) slug = `${base}-${n}`;
|
|
56
|
-
const l = await repo.createBookingLink({ resourceId: id, name, slug
|
|
59
|
+
const l = await repo.createBookingLink({ resourceId: id, name, slug });
|
|
57
60
|
return Astro.redirect(`/admin/calendar/resources/${id}/links/${l.id}`);
|
|
58
61
|
} else if (op === 'rotate') {
|
|
59
62
|
await repo.updateResource(id, { feedToken: token() });
|
|
@@ -65,14 +68,17 @@ if (Astro.request.method === 'POST') {
|
|
|
65
68
|
resource = (await repo.getResource(id))!;
|
|
66
69
|
}
|
|
67
70
|
const links = await repo.listBookingLinks(id);
|
|
71
|
+
// events holding this room in the next 90 days (lib/booking/holds.ts)
|
|
72
|
+
const holds = (await eventHolds(repo, settings, Date.now(), Date.now() + 90 * 86_400_000, id)).slice(0, 12);
|
|
73
|
+
const holdFmt = fmt(settings, resource.timezone || settings.timezone || 'UTC');
|
|
68
74
|
const staffFeed = origin + resourceFeedUrl(resource.slug, resource.feedToken);
|
|
69
75
|
const ovRows = [...resource.overrides, { date: '', endDate: '', ranges: [], label: '' }, { date: '', endDate: '', ranges: [], label: '' }];
|
|
70
76
|
---
|
|
71
77
|
|
|
72
|
-
<Admin title={resource.name} section="calendar:
|
|
78
|
+
<Admin title={resource.name} section="calendar:bookings">
|
|
73
79
|
<div class="page-head">
|
|
74
80
|
<div>
|
|
75
|
-
<p class="sub"><a href="/admin/calendar/resources">
|
|
81
|
+
<p class="sub"><a href="/admin/calendar/bookings">Bookings</a> › <a href="/admin/calendar/resources">Rooms & people</a> ›</p>
|
|
76
82
|
<h1>{resource.name}</h1>
|
|
77
83
|
<p class="sub"><a href={resourceUrl(resource.slug)} target="_blank" rel="noopener">{origin.replace(/^https?:\/\//, '')}{resourceUrl(resource.slug)} ↗</a></p>
|
|
78
84
|
</div>
|
|
@@ -89,7 +95,7 @@ const ovRows = [...resource.overrides, { date: '', endDate: '', ranges: [], labe
|
|
|
89
95
|
{links.map((l) => (
|
|
90
96
|
<a class="res-linkrow" href={`/admin/calendar/resources/${id}/links/${l.id}`}>
|
|
91
97
|
<span><b>{l.name}</b>{!l.active && <span class="pill lang">Off</span>}{l.visibility === 'unlisted' && <span class="pill lang">Unlisted</span>}</span>
|
|
92
|
-
<small>{describeLink(l)} · {l.mode === 'range' ? 'choose a length' : l.mode === 'day' ? 'whole days' : 'fixed length'} · <em class={
|
|
98
|
+
<small>{describeLink(l)} · {l.mode === 'range' ? 'choose a length' : l.mode === 'day' ? 'whole days' : 'fixed length'} · {(() => { const a = linkApproval(l, settings); return <em class={a.mode === 'auto' ? 'auto' : 'manual'}>{approvalShort(a)}{a.inherited ? ' (site setting)' : ''}</em>; })()}</small>
|
|
93
99
|
<code>{bookingLinkUrl(resource.slug, l.slug)}</code>
|
|
94
100
|
</a>
|
|
95
101
|
))}
|
|
@@ -97,10 +103,26 @@ const ovRows = [...resource.overrides, { date: '', endDate: '', ranges: [], labe
|
|
|
97
103
|
<form method="post" class="res-newlink">
|
|
98
104
|
<input type="hidden" name="op" value="link" />
|
|
99
105
|
<input type="text" name="linkName" placeholder="New link, e.g. “Event or reception”" required />
|
|
100
|
-
<button class="btn" type="submit">Add link</button>
|
|
106
|
+
<button class="btn" type="submit">Add booking link</button>
|
|
101
107
|
</form>
|
|
102
108
|
</section>
|
|
103
109
|
|
|
110
|
+
<section class="card card-pad">
|
|
111
|
+
<div class="card-head"><b>Held by events</b><span class="hint">Events that use this {resource.kind.toLowerCase()} can’t be booked over</span></div>
|
|
112
|
+
{holds.length === 0 ? (
|
|
113
|
+
<p class="hint" style="margin:0">No events are holding it in the next 90 days. Tick it under <b>Rooms</b> in an event’s settings to hold it.</p>
|
|
114
|
+
) : (
|
|
115
|
+
<div class="res-linklist">
|
|
116
|
+
{holds.map((h) => (
|
|
117
|
+
<a class="res-linkrow" href={`/admin/calendar/events/${h.eventId}`}>
|
|
118
|
+
<span><b>{h.title}</b></span>
|
|
119
|
+
<small>{holdFmt.day.format(h.start)} · {h.end - h.start >= 86_400_000 - 3_600_000 ? 'all day' : `${holdFmt.time.format(h.start)} – ${holdFmt.time.format(h.end)}`}</small>
|
|
120
|
+
</a>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</section>
|
|
125
|
+
|
|
104
126
|
<section class="card card-pad">
|
|
105
127
|
<div class="card-head"><b>Open hours</b><span class="hint">The outer limit for every link</span></div>
|
|
106
128
|
<HoursEditor hours={resource.hours} prefix="hours" form="res-form" note={`Times in ${resource.timezone || settings.timezone || 'UTC'}. Links can narrow these hours but never extend them.`} />
|