@aglyn/plugins-events-calendar 1.0.0-beta.260 → 1.0.0-beta.262
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-events-calendar",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.262",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
33
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
34
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
35
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.262",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.262",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.262",
|
|
31
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.262",
|
|
32
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.262",
|
|
33
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.262",
|
|
34
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.262",
|
|
35
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.262",
|
|
36
36
|
"@swc/helpers": "0.5.23"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
@@ -40,6 +40,13 @@ export interface EventItem {
|
|
|
40
40
|
*/
|
|
41
41
|
coverImageAlt?: string | null;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Where an event is and who hosts it, as its list line reads them
|
|
45
|
+
* (AGL-3856): the host is left out where the place already names it, so
|
|
46
|
+
* "Ironline Strength Studio, Austin, Texas" is never followed by "Ironline
|
|
47
|
+
* Strength Studio" again, and the place where the host's name holds it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function eventPlaceAndHost(event: Pick<EventItem, 'location' | 'organizer'>): string[];
|
|
43
50
|
/**
|
|
44
51
|
* One event as its `schema.org/Event` JSON-LD string (AGL-143/145).
|
|
45
52
|
*
|
|
@@ -27,6 +27,28 @@ import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
|
27
27
|
import { generatePresetId } from "../utils/generate-preset-id.js";
|
|
28
28
|
// Component ids are persisted in screen documents; never rename.
|
|
29
29
|
export const ID = 'eventList';
|
|
30
|
+
/**
|
|
31
|
+
* Where an event is and who hosts it, as its list line reads them
|
|
32
|
+
* (AGL-3856): the host is left out where the place already names it, so
|
|
33
|
+
* "Ironline Strength Studio, Austin, Texas" is never followed by "Ironline
|
|
34
|
+
* Strength Studio" again, and the place where the host's name holds it.
|
|
35
|
+
*/ export function eventPlaceAndHost(event) {
|
|
36
|
+
var _ref, _ref1;
|
|
37
|
+
var _event_location, _event_organizer;
|
|
38
|
+
const location = (_ref = (_event_location = event.location) == null ? void 0 : _event_location.replace(/\s+/g, ' ').trim()) != null ? _ref : '';
|
|
39
|
+
const organizer = (_ref1 = (_event_organizer = event.organizer) == null ? void 0 : _event_organizer.replace(/\s+/g, ' ').trim()) != null ? _ref1 : '';
|
|
40
|
+
const names = (outer, inner)=>outer.toLowerCase().includes(inner.toLowerCase());
|
|
41
|
+
if (location && organizer && names(location, organizer)) return [
|
|
42
|
+
location
|
|
43
|
+
];
|
|
44
|
+
if (location && organizer && names(organizer, location)) return [
|
|
45
|
+
organizer
|
|
46
|
+
];
|
|
47
|
+
return [
|
|
48
|
+
location,
|
|
49
|
+
organizer
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
}
|
|
30
52
|
/**
|
|
31
53
|
* One event as its `schema.org/Event` JSON-LD string (AGL-143/145).
|
|
32
54
|
*
|
|
@@ -202,8 +224,7 @@ export const ID = 'eventList';
|
|
|
202
224
|
dateStyle: 'medium',
|
|
203
225
|
timeStyle: 'short'
|
|
204
226
|
}),
|
|
205
|
-
event.
|
|
206
|
-
event.organizer ? ` · ${event.organizer}` : ''
|
|
227
|
+
eventPlaceAndHost(event).map((part)=>` · ${part}`).join('')
|
|
207
228
|
]
|
|
208
229
|
}),
|
|
209
230
|
event.description ? /*#__PURE__*/ _jsx(Typography, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/components/event-list.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCalendarStar } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport CircularProgress from '@mui/material/CircularProgress'\nimport Stack from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useEffect, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'eventList'\n\nexport interface EventListProps {\n /** 'upcoming' (default) or 'past'. */\n mode?: 'upcoming' | 'past'\n /** Heading above the list; empty hides it. */\n heading?: string\n /** Max events shown (default 10). */\n maxItems?: number\n}\n\nexport interface EventItem {\n $id: string\n title: string\n startsAtMs: number\n endsAtMs: number\n location?: string | null\n organizer?: string | null\n description?: string | null\n coverImage?: string | null\n /**\n * The author's description of the cover (AGL-2418). Absent or blank\n * renders `alt=\"\"` — correct for this layout, where the thumbnail sits\n * immediately beside the title that already names it.\n */\n coverImageAlt?: string | null\n}\n\n/**\n * One event as its `schema.org/Event` JSON-LD string (AGL-143/145).\n *\n * Lifted out of the JSX so the structured data can be asserted on its own,\n * which is what the surface is FOR — it is markup no human ever sees, so a\n * render test that only checks the visible list proves nothing about it.\n */\nexport function eventJsonLd(event: EventItem): string {\n // The cover, and only if a crawler could actually fetch it (AGL-1351).\n //\n // Calling the shared resolver with NO host is how this surface says \"accept\n // what is already absolute, and nothing else\": with no origin to resolve\n // against it returns undefined for a site-relative path and for a `media:`\n // reference alike, so neither can reach the markup. The origin lives on the\n // host record, which only the server holds — `/api/events/list` resolves\n // each cover there, so in practice an already-absolute URL passes straight\n // through and this stands as the guarantee that the node is never emitted\n // with an address a crawler cannot follow, whatever the payload contains.\n //\n // Deliberately NOT `window.location.origin`: on the console's Preview\n // surface that is the console, and the markup would then name this site's\n // image on a URL belonging to a different one.\n const image = Aglyn.resolveSocialImage({\n sources: [{ image: event.coverImage }],\n host: undefined,\n })\n return Aglyn.safeJsonLd({\n '@context': 'https://schema.org',\n '@type': 'Event',\n name: event.title,\n startDate: new Date(event.startsAtMs).toISOString(),\n ...(event.endsAtMs && {\n endDate: new Date(event.endsAtMs).toISOString(),\n }),\n ...(event.location && {\n location: { '@type': 'Place', name: event.location },\n }),\n ...(event.organizer && {\n organizer: {\n '@type': 'Organization',\n name: event.organizer,\n },\n }),\n ...(event.description && {\n description: event.description,\n }),\n // Absent rather than `\"image\": [null]` — `strictNullChecks` is off\n // repo-wide, so this guard is what keeps an unresolvable cover out.\n ...(image && { image: [image.url] }),\n })\n}\n\n/**\n * Event List (AGL-145, Event Calendar add-on): published events from the\n * public tenant API with schema.org Event JSON-LD per item (ties into the\n * AGL-143 structured-data work). Editor placeholder without a site\n * context; empty add-on hosts render nothing live.\n */\nconst EventList = forwardRef<HTMLDivElement, EventListProps>((props, ref) => {\n const { mode, heading, maxItems, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const [events, setEvents] = useState<EventItem[] | null>(null)\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(\n `/api/events/list?hostId=${encodeURIComponent(hostId)}` +\n `&mode=${mode === 'past' ? 'past' : 'upcoming'}`,\n )\n .then((response) => response.json())\n .then((payload) => {\n if (active) setEvents(payload?.events ?? [])\n })\n .catch(() => {\n if (active) setEvents([])\n })\n return () => {\n active = false\n }\n }, [hostId, mode])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Event list — published events render here (Event Calendar add-on)'}\n </Box>\n )\n }\n if (events === null) {\n return (\n <Box ref={ref} {...rest} sx={[{ p: 2 }, ...nodeSx]}>\n <CircularProgress size={24} />\n </Box>\n )\n }\n const visible = events.slice(0, maxItems && maxItems > 0 ? maxItems : 10)\n if (!visible.length) return <Box ref={ref} {...rest} />\n\n return (\n <Stack ref={ref} spacing={2} {...rest}>\n {heading ? <Typography variant=\"h5\">{heading}</Typography> : null}\n {visible.map((event) => (\n <Stack\n key={event.$id}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'flex-start' }}\n >\n {/* The cover is a free-text console field rendered verbatim — one\n of AGL-1725's raw author sinks. Scheme rule only, never a host\n check: `http:` (an insecure egress no browser defends) and\n unknown schemes render nothing; https/relative/data render as\n stored. */}\n {event.coverImage &&\n !Aglyn.isRefusedAuthorImageSrc(event.coverImage) ? (\n <Box\n component=\"img\"\n src={event.coverImage}\n // The author's own description when they wrote one (AGL-2418),\n // and `alt=\"\"` otherwise. No fallback to the title: this is a\n // 96×96 thumbnail sitting immediately beside that title, so\n // borrowing it would announce the same words twice.\n alt={Aglyn.renderedMediaAlt(event.coverImageAlt)}\n sx={{\n width: 96,\n height: 96,\n objectFit: 'cover',\n borderRadius: 1,\n flexShrink: 0,\n }}\n // Deferred (AGL-2486). A 96x96 thumbnail, one per event, in a\n // list that is as long as the calendar is busy — previously\n // unhinted, so a twenty-event list opened twenty eager\n // default-priority fetches.\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n ) : null}\n <Stack spacing={0.25} sx={{ minWidth: 0 }}>\n <Typography variant=\"h6\">{event.title}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {new Date(event.startsAtMs).toLocaleString([], {\n dateStyle: 'medium',\n timeStyle: 'short',\n })}\n {event.location ? ` · ${event.location}` : ''}\n {event.organizer ? ` · ${event.organizer}` : ''}\n </Typography>\n {event.description ? (\n <Typography variant=\"body2\">{event.description}</Typography>\n ) : null}\n </Stack>\n {/* schema.org Event (AGL-143/145), built above. */}\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: eventJsonLd(event) }}\n />\n </Stack>\n ))}\n </Stack>\n )\n})\nEventList.displayName = 'EventList'\n\nexport const schema: Aglyn.ComponentSchema<EventListProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Event List',\n description:\n 'Your published events, each marked up so search engines can show it.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: {\n path: mdiCalendarStar.path,\n sx: { color: '#7b1fa2' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'heading',\n description: 'Heading above the list; empty hides it.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Heading',\n },\n {\n name: 'mode',\n description: 'Which events to show.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Show',\n options: [\n { value: '', label: 'Upcoming (default)' },\n { value: 'past', label: 'Past events' },\n ],\n },\n {\n name: 'maxItems',\n description: 'Maximum events shown (default 10).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Max items',\n type: 'number',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Event List',\n pluginId: BUNDLE_ID,\n description: 'Published events with SEO Event markup (add-on)',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: {\n path: mdiCalendarStar.path,\n sx: { color: '#7b1fa2' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default EventList\n"],"names":["Aglyn","mdiCalendarStar","Box","CircularProgress","Stack","Typography","forwardRef","useEffect","useState","BUNDLE_ID","generatePresetId","ID","eventJsonLd","event","image","resolveSocialImage","sources","coverImage","host","undefined","safeJsonLd","name","title","startDate","Date","startsAtMs","toISOString","endsAtMs","endDate","location","organizer","description","url","EventList","props","ref","mode","heading","maxItems","rest","nodeSx","Array","isArray","hostId","useSite","events","setEvents","active","fetch","encodeURIComponent","then","response","json","payload","catch","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","size","visible","slice","length","spacing","variant","map","direction","alignItems","isRefusedAuthorImageSrc","component","src","alt","renderedMediaAlt","coverImageAlt","width","height","objectFit","flexShrink","DEFERRED_IMAGE_ATTRIBUTES","minWidth","toLocaleString","dateStyle","timeStyle","script","type","dangerouslySetInnerHTML","__html","$id","displayName","schema","pluginId","category","ComponentCategory","DATA_DISPLAY","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","FieldComponentType","TEXT_FIELD","label","SELECT","options","value","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/components/event-list.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCalendarStar } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport CircularProgress from '@mui/material/CircularProgress'\nimport Stack from '@mui/material/Stack'\nimport Typography from '@mui/material/Typography'\nimport { forwardRef, useEffect, useState } from 'react'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { generatePresetId } from '../utils/generate-preset-id'\n\n// Component ids are persisted in screen documents; never rename.\nexport const ID: Aglyn.ComponentId = 'eventList'\n\nexport interface EventListProps {\n /** 'upcoming' (default) or 'past'. */\n mode?: 'upcoming' | 'past'\n /** Heading above the list; empty hides it. */\n heading?: string\n /** Max events shown (default 10). */\n maxItems?: number\n}\n\nexport interface EventItem {\n $id: string\n title: string\n startsAtMs: number\n endsAtMs: number\n location?: string | null\n organizer?: string | null\n description?: string | null\n coverImage?: string | null\n /**\n * The author's description of the cover (AGL-2418). Absent or blank\n * renders `alt=\"\"` — correct for this layout, where the thumbnail sits\n * immediately beside the title that already names it.\n */\n coverImageAlt?: string | null\n}\n\n/**\n * Where an event is and who hosts it, as its list line reads them\n * (AGL-3856): the host is left out where the place already names it, so\n * \"Ironline Strength Studio, Austin, Texas\" is never followed by \"Ironline\n * Strength Studio\" again, and the place where the host's name holds it.\n */\nexport function eventPlaceAndHost(event: Pick<EventItem, 'location' | 'organizer'>): string[] {\n const location = event.location?.replace(/\\s+/g, ' ').trim() ?? ''\n const organizer = event.organizer?.replace(/\\s+/g, ' ').trim() ?? ''\n const names = (outer: string, inner: string) => outer.toLowerCase().includes(inner.toLowerCase())\n if (location && organizer && names(location, organizer)) return [location]\n if (location && organizer && names(organizer, location)) return [organizer]\n return [location, organizer].filter(Boolean)\n}\n\n/**\n * One event as its `schema.org/Event` JSON-LD string (AGL-143/145).\n *\n * Lifted out of the JSX so the structured data can be asserted on its own,\n * which is what the surface is FOR — it is markup no human ever sees, so a\n * render test that only checks the visible list proves nothing about it.\n */\nexport function eventJsonLd(event: EventItem): string {\n // The cover, and only if a crawler could actually fetch it (AGL-1351).\n //\n // Calling the shared resolver with NO host is how this surface says \"accept\n // what is already absolute, and nothing else\": with no origin to resolve\n // against it returns undefined for a site-relative path and for a `media:`\n // reference alike, so neither can reach the markup. The origin lives on the\n // host record, which only the server holds — `/api/events/list` resolves\n // each cover there, so in practice an already-absolute URL passes straight\n // through and this stands as the guarantee that the node is never emitted\n // with an address a crawler cannot follow, whatever the payload contains.\n //\n // Deliberately NOT `window.location.origin`: on the console's Preview\n // surface that is the console, and the markup would then name this site's\n // image on a URL belonging to a different one.\n const image = Aglyn.resolveSocialImage({\n sources: [{ image: event.coverImage }],\n host: undefined,\n })\n return Aglyn.safeJsonLd({\n '@context': 'https://schema.org',\n '@type': 'Event',\n name: event.title,\n startDate: new Date(event.startsAtMs).toISOString(),\n ...(event.endsAtMs && {\n endDate: new Date(event.endsAtMs).toISOString(),\n }),\n ...(event.location && {\n location: { '@type': 'Place', name: event.location },\n }),\n ...(event.organizer && {\n organizer: {\n '@type': 'Organization',\n name: event.organizer,\n },\n }),\n ...(event.description && {\n description: event.description,\n }),\n // Absent rather than `\"image\": [null]` — `strictNullChecks` is off\n // repo-wide, so this guard is what keeps an unresolvable cover out.\n ...(image && { image: [image.url] }),\n })\n}\n\n/**\n * Event List (AGL-145, Event Calendar add-on): published events from the\n * public tenant API with schema.org Event JSON-LD per item (ties into the\n * AGL-143 structured-data work). Editor placeholder without a site\n * context; empty add-on hosts render nothing live.\n */\nconst EventList = forwardRef<HTMLDivElement, EventListProps>((props, ref) => {\n const { mode, heading, maxItems, ...rest } = props\n // Node styles ride the renderer-merged sx; recompose (stack.ts pattern).\n const nodeSx = Array.isArray(props['sx']) ? props['sx'] : [props['sx']]\n const { hostId } = Aglyn.useSite()\n const [events, setEvents] = useState<EventItem[] | null>(null)\n\n useEffect(() => {\n if (!hostId) return\n let active = true\n void fetch(\n `/api/events/list?hostId=${encodeURIComponent(hostId)}` +\n `&mode=${mode === 'past' ? 'past' : 'upcoming'}`,\n )\n .then((response) => response.json())\n .then((payload) => {\n if (active) setEvents(payload?.events ?? [])\n })\n .catch(() => {\n if (active) setEvents([])\n })\n return () => {\n active = false\n }\n }, [hostId, mode])\n\n if (!hostId) {\n return (\n <Box\n ref={ref}\n {...rest}\n sx={[\n {\n p: 3,\n border: '1px dashed',\n borderColor: 'divider',\n borderRadius: 1,\n color: 'text.secondary',\n fontSize: 13,\n fontFamily: 'system-ui, sans-serif',\n },\n ...nodeSx,\n ]}\n >\n {'Event list — published events render here (Event Calendar add-on)'}\n </Box>\n )\n }\n if (events === null) {\n return (\n <Box ref={ref} {...rest} sx={[{ p: 2 }, ...nodeSx]}>\n <CircularProgress size={24} />\n </Box>\n )\n }\n const visible = events.slice(0, maxItems && maxItems > 0 ? maxItems : 10)\n if (!visible.length) return <Box ref={ref} {...rest} />\n\n return (\n <Stack ref={ref} spacing={2} {...rest}>\n {heading ? <Typography variant=\"h5\">{heading}</Typography> : null}\n {visible.map((event) => (\n <Stack\n key={event.$id}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'flex-start' }}\n >\n {/* The cover is a free-text console field rendered verbatim — one\n of AGL-1725's raw author sinks. Scheme rule only, never a host\n check: `http:` (an insecure egress no browser defends) and\n unknown schemes render nothing; https/relative/data render as\n stored. */}\n {event.coverImage &&\n !Aglyn.isRefusedAuthorImageSrc(event.coverImage) ? (\n <Box\n component=\"img\"\n src={event.coverImage}\n // The author's own description when they wrote one (AGL-2418),\n // and `alt=\"\"` otherwise. No fallback to the title: this is a\n // 96×96 thumbnail sitting immediately beside that title, so\n // borrowing it would announce the same words twice.\n alt={Aglyn.renderedMediaAlt(event.coverImageAlt)}\n sx={{\n width: 96,\n height: 96,\n objectFit: 'cover',\n borderRadius: 1,\n flexShrink: 0,\n }}\n // Deferred (AGL-2486). A 96x96 thumbnail, one per event, in a\n // list that is as long as the calendar is busy — previously\n // unhinted, so a twenty-event list opened twenty eager\n // default-priority fetches.\n {...Aglyn.DEFERRED_IMAGE_ATTRIBUTES}\n />\n ) : null}\n <Stack spacing={0.25} sx={{ minWidth: 0 }}>\n <Typography variant=\"h6\">{event.title}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {new Date(event.startsAtMs).toLocaleString([], {\n dateStyle: 'medium',\n timeStyle: 'short',\n })}\n {eventPlaceAndHost(event).map((part) => ` · ${part}`).join('')}\n </Typography>\n {event.description ? (\n <Typography variant=\"body2\">{event.description}</Typography>\n ) : null}\n </Stack>\n {/* schema.org Event (AGL-143/145), built above. */}\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: eventJsonLd(event) }}\n />\n </Stack>\n ))}\n </Stack>\n )\n})\nEventList.displayName = 'EventList'\n\nexport const schema: Aglyn.ComponentSchema<EventListProps> = {\n $id: ID,\n pluginId: BUNDLE_ID,\n displayName: 'Event List',\n description:\n 'Your published events, each marked up so search engines can show it.',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: {\n path: mdiCalendarStar.path,\n sx: { color: '#7b1fa2' },\n },\n flags: {\n selfClosing: Aglyn.FEATURE_FLAG.ENABLED,\n },\n attributes: [\n {\n name: 'heading',\n description: 'Heading above the list; empty hides it.',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Heading',\n },\n {\n name: 'mode',\n description: 'Which events to show.',\n component: Aglyn.FieldComponentType.SELECT,\n label: 'Show',\n options: [\n { value: '', label: 'Upcoming (default)' },\n { value: 'past', label: 'Past events' },\n ],\n },\n {\n name: 'maxItems',\n description: 'Maximum events shown (default 10).',\n component: Aglyn.FieldComponentType.TEXT_FIELD,\n label: 'Max items',\n type: 'number',\n },\n ],\n}\n\nexport const presets: Aglyn.PresetSchema[] = [\n {\n $id: generatePresetId(ID),\n type: Aglyn.NodeType.PRESET,\n displayName: 'Event List',\n pluginId: BUNDLE_ID,\n description: 'Published events with SEO Event markup (add-on)',\n category: Aglyn.ComponentCategory.DATA_DISPLAY,\n icon: {\n path: mdiCalendarStar.path,\n sx: { color: '#7b1fa2' },\n },\n data: {\n $id: null,\n componentId: ID,\n pluginId: BUNDLE_ID,\n props: {},\n },\n },\n]\n\nexport default EventList\n"],"names":["Aglyn","mdiCalendarStar","Box","CircularProgress","Stack","Typography","forwardRef","useEffect","useState","BUNDLE_ID","generatePresetId","ID","eventPlaceAndHost","event","location","replace","trim","organizer","names","outer","inner","toLowerCase","includes","filter","Boolean","eventJsonLd","image","resolveSocialImage","sources","coverImage","host","undefined","safeJsonLd","name","title","startDate","Date","startsAtMs","toISOString","endsAtMs","endDate","description","url","EventList","props","ref","mode","heading","maxItems","rest","nodeSx","Array","isArray","hostId","useSite","events","setEvents","active","fetch","encodeURIComponent","then","response","json","payload","catch","sx","p","border","borderColor","borderRadius","color","fontSize","fontFamily","size","visible","slice","length","spacing","variant","map","direction","alignItems","isRefusedAuthorImageSrc","component","src","alt","renderedMediaAlt","coverImageAlt","width","height","objectFit","flexShrink","DEFERRED_IMAGE_ATTRIBUTES","minWidth","toLocaleString","dateStyle","timeStyle","part","join","script","type","dangerouslySetInnerHTML","__html","$id","displayName","schema","pluginId","category","ComponentCategory","DATA_DISPLAY","icon","path","flags","selfClosing","FEATURE_FLAG","ENABLED","attributes","FieldComponentType","TEXT_FIELD","label","SELECT","options","value","presets","NodeType","PRESET","data","componentId"],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
|
19
19
|
import { stockPhotoPicker } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
|
|
20
20
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
21
21
|
import { checkEventDraftContent, EVENT_DRAFT_COVER_ADDRESS, eventDraftWallText, readEventDraftContent } from "../model/event-draft-content.js";
|
|
22
|
-
import { eventWrite } from "../model/event-write.js";
|
|
22
|
+
import { EVENT_COVER_ALT_MAX_LENGTH, eventWrite } from "../model/event-write.js";
|
|
23
23
|
/** The refusal for a member who may not write the site. */ export const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role';
|
|
24
24
|
/** The refusal for a workspace without the add-on, in the Events page's words. */ export const EVENT_DRAFT_PLAN_REFUSAL = 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons';
|
|
25
25
|
/** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */ export const EVENT_DRAFT_UNPUBLISHED_NOTICE = 'Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.';
|
|
@@ -68,10 +68,16 @@ function planRefusal(org) {
|
|
|
68
68
|
error: EVENT_DRAFT_PLAN_REFUSAL
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* A cover for an event the content gave none: the picker's, held to what the
|
|
73
|
+
* editor's field takes, with the words that describe it (AGL-3856): a cover
|
|
74
|
+
* the event list drew with `alt=""` hid a yoga pose under "Open gym strength
|
|
75
|
+
* session" from a screen reader.
|
|
76
|
+
*/ async function coverFor(picker, request, title) {
|
|
72
77
|
if (!picker) return null;
|
|
73
78
|
try {
|
|
74
|
-
var
|
|
79
|
+
var _ref;
|
|
80
|
+
var _request_act, _request_act1, _pick_alt;
|
|
75
81
|
const pick = await picker.pick(_extends({
|
|
76
82
|
hostId: request.hostId,
|
|
77
83
|
uid: request.uid,
|
|
@@ -86,7 +92,12 @@ function planRefusal(org) {
|
|
|
86
92
|
// The draft's id: asked again, the same photo.
|
|
87
93
|
seed: request.id
|
|
88
94
|
}));
|
|
89
|
-
|
|
95
|
+
if (!pick || !EVENT_DRAFT_COVER_ADDRESS.test(pick.url)) return null;
|
|
96
|
+
const alt = (_ref = (_pick_alt = pick.alt) == null ? void 0 : _pick_alt.replace(/\s+/g, ' ').trim().slice(0, EVENT_COVER_ALT_MAX_LENGTH)) != null ? _ref : '';
|
|
97
|
+
return {
|
|
98
|
+
src: pick.url,
|
|
99
|
+
alt: alt || null
|
|
100
|
+
};
|
|
90
101
|
} catch (error) {
|
|
91
102
|
console.warn('event draft: no cover photo', {
|
|
92
103
|
error: String(error)
|
|
@@ -143,7 +154,10 @@ export function createEventDraftWriter(deps = {}) {
|
|
|
143
154
|
]);
|
|
144
155
|
if (host.exists && !existing.exists && !roleRefusal(host, request.uid) && !planRefusal(request.org)) {
|
|
145
156
|
const cover = await coverFor(stockPhotos(), request, read.value.title);
|
|
146
|
-
if (cover)
|
|
157
|
+
if (cover) {
|
|
158
|
+
read.value.coverImage = cover.src;
|
|
159
|
+
if (cover.alt && !read.value.coverImageAlt) read.value.coverImageAlt = cover.alt;
|
|
160
|
+
}
|
|
147
161
|
}
|
|
148
162
|
}
|
|
149
163
|
return db.runTransaction(async (tx)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-drafts.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanPublish, hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftRequest,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n stockPhotoPicker,\n type StockPhotoPicker,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n checkEventDraftContent,\n EVENT_DRAFT_COVER_ADDRESS,\n eventDraftWallText,\n readEventDraftContent,\n} from '../model/event-draft-content'\nimport { eventWrite } from '../model/event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).\n *\n * The draft writer this plugin registers for the `event` resource on the\n * core's resource-drafts seam. A plugin that sets a site up from a brief —\n * an AI build — asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through\n * `eventWrite`, the one rule the Events page's editor and the events\n * import write through.\n * - PUBLISHED when the member may publish (AGL-3755): the Events page's\n * \"Set published\" is the event's `status` set to `published` and nothing\n * else, so that is all this writer changes. The member must hold the\n * publishing role (`hostRoleCanPublish`, admin or editor), the axis every\n * other publish asks. A member who may write but not publish (`author`)\n * gets the event as a draft, `live: false`, with a notice saying why, and\n * it waits on the Events page for someone who may.\n * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read\n * from the workspace's own document the way the listing and the import\n * read it. There is no allowance to count.\n * - THE ROLE is a member who may write the site's content.\n * - THE START is after the write. A draft that would open on the Events page\n * already over is a mistake in what was asked, so it is refused rather\n * than stored.\n * - THE COVER (AGL-3755), when the content names none: a photo of the\n * event's title from the deployment's stock photo picker\n * (`core.stock-photo-picker`), kept in the site's own media library once\n * and reused after, or a starter photo when the library is down. It is\n * the editor's own \"Cover image URL\" field, `coverImage`, holding the\n * photo's site-relative address. Photos of one act (the AI build) never\n * repeat. A process with no picker writes the event without a cover.\n *\n * The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusal for a member who may not write the site. */\nexport const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** The refusal for a workspace without the add-on, in the Events page's words. */\nexport const EVENT_DRAFT_PLAN_REFUSAL =\n 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons'\n\n/** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */\nexport const EVENT_DRAFT_UNPUBLISHED_NOTICE =\n 'Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.'\n\n/** The refusal for a start that is not after the write. */\nexport function eventDraftPastRefusal(wallText: string): string {\n return `A drafted event starts in the future: ${wallText} has already passed`\n}\n\nfunction isDeleted(data: Readonly<Record<string, unknown>> | undefined): boolean {\n return Boolean(data?.['deletedAt']) || data?.['status'] === 'deleted'\n}\n\n/** What a written event reports: published or a draft, and why a draft when it is one. */\nfunction liveOf(published: boolean): Pick<PluginDraftRecord, 'live' | 'notice'> {\n return published ? { live: true } : { live: false, notice: EVENT_DRAFT_UNPUBLISHED_NOTICE }\n}\n\nfunction recordOf(event: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (event.data() ?? {}) as Record<string, unknown>\n const published = data['status'] === 'published'\n return {\n id: event.id,\n name: String(data['title'] ?? ''),\n versionId: null,\n facts: {\n status: published ? 'published' : 'draft',\n startsAtMs: Number(data['startsAtMs'] ?? 0),\n endsAtMs: Number(data['endsAtMs'] ?? 0),\n ...(typeof data['coverImage'] === 'string' ? { coverImage: data['coverImage'] } : {}),\n },\n ...liveOf(published),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EVENT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'eventCalendar')\n ? null\n : { status: 403, error: EVENT_DRAFT_PLAN_REFUSAL }\n}\n\nexport interface EventDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** The stock photo picker a cover comes from; specs hand in a double. */\n stockPhotos?: () => StockPhotoPicker | null\n}\n\n/** A cover for an event the content gave none: the picker's, held to what the editor's field takes. */\nasync function coverFor(\n picker: StockPhotoPicker | null,\n request: PluginDraftRequest,\n title: string,\n): Promise<string | null> {\n if (!picker) return null\n try {\n const pick = await picker.pick({\n hostId: request.hostId,\n uid: request.uid,\n subject: title,\n ...(request.act?.business ? { business: request.act.business } : {}),\n orientation: 'horizontal',\n ...(request.act?.id ? { scope: request.act.id } : {}),\n // The draft's id: asked again, the same photo.\n seed: request.id,\n })\n return pick && EVENT_DRAFT_COVER_ADDRESS.test(pick.url) ? pick.url : null\n } catch (error) {\n console.warn('event draft: no cover photo', { error: String(error) })\n return null\n }\n}\n\nexport function createEventDraftWriter(deps: EventDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const stockPhotos = deps.stockPhotos ?? stockPhotoPicker\n return {\n refusal: async (context) => {\n const host = await firestore().collection('hosts').doc(context.hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return roleRefusal(host, context.uid) ?? planRefusal(context.org)\n },\n\n check: (content) => checkEventDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get()\n return event.exists && !isDeleted(event.data()) ? recordOf(event) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = request.name.replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n const read = readEventDraftContent(asked ? { ...request.content, title: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (read.value.startsAtMs <= request.now.getTime()) {\n return {\n ok: false,\n status: 400,\n error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone)),\n }\n }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const eventRef = hostRef.collection('events').doc(request.id)\n // A cover is fetched only for a new event a member may write, before\n // the transaction: a photo's copy is too slow to hold one open.\n if (!read.value.coverImage) {\n const [host, existing] = await Promise.all([hostRef.get(), eventRef.get()])\n if (host.exists && !existing.exists && !roleRefusal(host, request.uid) && !planRefusal(request.org)) {\n const cover = await coverFor(stockPhotos(), request, read.value.title)\n if (cover) read.value.coverImage = cover\n }\n }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(eventRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n // Asked again under the same id: the draft it already wrote — unless\n // a person deleted it since, which a replay never brings back.\n return isDeleted(existing.data())\n ? { ok: false, status: 409, error: 'That event was deleted on the Events page' }\n : { ok: true, replayed: true, ...recordOf(existing) }\n }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n // The Events page's publish: `status`, and only for the publishing role.\n const published = hostRoleCanPublish((host.get('memberRoles') ?? {})[request.uid])\n const write = eventWrite({ ...read.value, status: published ? 'published' : 'draft' })\n tx.create(eventRef, {\n ...write.fields,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: write.fields.title,\n versionId: null,\n facts: {\n status: write.fields.status,\n startsAtMs: write.fields.startsAtMs,\n endsAtMs: write.fields.endsAtMs,\n ...(write.fields.coverImage ? { coverImage: write.fields.coverImage } : {}),\n },\n ...liveOf(published),\n }\n })\n },\n }\n}\n\nexport const eventDraftWriter = createEventDraftWriter()\n"],"names":["hostRoleCanPublish","hostRoleCanWrite","checkEntitlement","stockPhotoPicker","firebaseAdmin","checkEventDraftContent","EVENT_DRAFT_COVER_ADDRESS","eventDraftWallText","readEventDraftContent","eventWrite","EVENT_DRAFT_ROLE_REFUSAL","EVENT_DRAFT_PLAN_REFUSAL","EVENT_DRAFT_UNPUBLISHED_NOTICE","eventDraftPastRefusal","wallText","isDeleted","data","Boolean","liveOf","published","live","notice","recordOf","event","id","name","String","versionId","facts","status","startsAtMs","Number","endsAtMs","coverImage","roleRefusal","host","uid","role","get","error","planRefusal","org","coverFor","picker","request","title","pick","hostId","subject","act","business","orientation","scope","seed","test","url","console","warn","createEventDraftWriter","deps","firestore","app","stockPhotos","refusal","context","collection","doc","exists","check","content","read","write","asked","replace","trim","ok","problems","value","now","getTime","timezone","db","hostRef","eventRef","existing","Promise","all","cover","runTransaction","tx","replayed","early","create","fields","createdAt","updatedAt","createdBy","eventDraftWriter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/events-calendar/src/lib/server/event-drafts.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanPublish, hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftRequest,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n stockPhotoPicker,\n type StockPhotoPicker,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n checkEventDraftContent,\n EVENT_DRAFT_COVER_ADDRESS,\n eventDraftWallText,\n readEventDraftContent,\n} from '../model/event-draft-content'\nimport { EVENT_COVER_ALT_MAX_LENGTH, eventWrite } from '../model/event-write'\n\n/**\n * AN EVENT ANOTHER PLUGIN ASKS FOR (AGL-3743).\n *\n * The draft writer this plugin registers for the `event` resource on the\n * core's resource-drafts seam. A plugin that sets a site up from a brief —\n * an AI build — asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/events/{id}` event stored through\n * `eventWrite`, the one rule the Events page's editor and the events\n * import write through.\n * - PUBLISHED when the member may publish (AGL-3755): the Events page's\n * \"Set published\" is the event's `status` set to `published` and nothing\n * else, so that is all this writer changes. The member must hold the\n * publishing role (`hostRoleCanPublish`, admin or editor), the axis every\n * other publish asks. A member who may write but not publish (`author`)\n * gets the event as a draft, `live: false`, with a notice saying why, and\n * it waits on the Events page for someone who may.\n * - THE PLAN is the `eventCalendar` add-on the Events page is gated on, read\n * from the workspace's own document the way the listing and the import\n * read it. There is no allowance to count.\n * - THE ROLE is a member who may write the site's content.\n * - THE START is after the write. A draft that would open on the Events page\n * already over is a mistake in what was asked, so it is refused rather\n * than stored.\n * - THE COVER (AGL-3755), when the content names none: a photo of the\n * event's title from the deployment's stock photo picker\n * (`core.stock-photo-picker`), kept in the site's own media library once\n * and reused after, or a starter photo when the library is down. It is\n * the editor's own \"Cover image URL\" field, `coverImage`, holding the\n * photo's site-relative address. Photos of one act (the AI build) never\n * repeat. A process with no picker writes the event without a cover.\n *\n * The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusal for a member who may not write the site. */\nexport const EVENT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n\n/** The refusal for a workspace without the add-on, in the Events page's words. */\nexport const EVENT_DRAFT_PLAN_REFUSAL =\n 'The Event Calendar is a paid add-on: enable it from Billing → Add-ons'\n\n/** Why an event a member wrote was kept as a draft: they may write the site, not publish it. */\nexport const EVENT_DRAFT_UNPUBLISHED_NOTICE =\n 'Kept as a draft: publishing an event takes the editor role, so someone who has it publishes it on the Events page.'\n\n/** The refusal for a start that is not after the write. */\nexport function eventDraftPastRefusal(wallText: string): string {\n return `A drafted event starts in the future: ${wallText} has already passed`\n}\n\nfunction isDeleted(data: Readonly<Record<string, unknown>> | undefined): boolean {\n return Boolean(data?.['deletedAt']) || data?.['status'] === 'deleted'\n}\n\n/** What a written event reports: published or a draft, and why a draft when it is one. */\nfunction liveOf(published: boolean): Pick<PluginDraftRecord, 'live' | 'notice'> {\n return published ? { live: true } : { live: false, notice: EVENT_DRAFT_UNPUBLISHED_NOTICE }\n}\n\nfunction recordOf(event: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (event.data() ?? {}) as Record<string, unknown>\n const published = data['status'] === 'published'\n return {\n id: event.id,\n name: String(data['title'] ?? ''),\n versionId: null,\n facts: {\n status: published ? 'published' : 'draft',\n startsAtMs: Number(data['startsAtMs'] ?? 0),\n endsAtMs: Number(data['endsAtMs'] ?? 0),\n ...(typeof data['coverImage'] === 'string' ? { coverImage: data['coverImage'] } : {}),\n },\n ...liveOf(published),\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: EVENT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'eventCalendar')\n ? null\n : { status: 403, error: EVENT_DRAFT_PLAN_REFUSAL }\n}\n\nexport interface EventDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n /** The stock photo picker a cover comes from; specs hand in a double. */\n stockPhotos?: () => StockPhotoPicker | null\n}\n\n/**\n * A cover for an event the content gave none: the picker's, held to what the\n * editor's field takes, with the words that describe it (AGL-3856): a cover\n * the event list drew with `alt=\"\"` hid a yoga pose under \"Open gym strength\n * session\" from a screen reader.\n */\nasync function coverFor(\n picker: StockPhotoPicker | null,\n request: PluginDraftRequest,\n title: string,\n): Promise<{ src: string; alt: string | null } | null> {\n if (!picker) return null\n try {\n const pick = await picker.pick({\n hostId: request.hostId,\n uid: request.uid,\n subject: title,\n ...(request.act?.business ? { business: request.act.business } : {}),\n orientation: 'horizontal',\n ...(request.act?.id ? { scope: request.act.id } : {}),\n // The draft's id: asked again, the same photo.\n seed: request.id,\n })\n if (!pick || !EVENT_DRAFT_COVER_ADDRESS.test(pick.url)) return null\n const alt = pick.alt?.replace(/\\s+/g, ' ').trim().slice(0, EVENT_COVER_ALT_MAX_LENGTH) ?? ''\n return { src: pick.url, alt: alt || null }\n } catch (error) {\n console.warn('event draft: no cover photo', { error: String(error) })\n return null\n }\n}\n\nexport function createEventDraftWriter(deps: EventDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n const stockPhotos = deps.stockPhotos ?? stockPhotoPicker\n return {\n refusal: async (context) => {\n const host = await firestore().collection('hosts').doc(context.hostId).get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n return roleRefusal(host, context.uid) ?? planRefusal(context.org)\n },\n\n check: (content) => checkEventDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const event = await firestore().collection('hosts').doc(hostId).collection('events').doc(id).get()\n return event.exists && !isDeleted(event.data()) ? recordOf(event) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = request.name.replace(/[\\p{Cc}\\s]+/gu, ' ').trim()\n const read = readEventDraftContent(asked ? { ...request.content, title: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n if (read.value.startsAtMs <= request.now.getTime()) {\n return {\n ok: false,\n status: 400,\n error: eventDraftPastRefusal(eventDraftWallText(read.value.startsAtMs, read.timezone)),\n }\n }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const eventRef = hostRef.collection('events').doc(request.id)\n // A cover is fetched only for a new event a member may write, before\n // the transaction: a photo's copy is too slow to hold one open.\n if (!read.value.coverImage) {\n const [host, existing] = await Promise.all([hostRef.get(), eventRef.get()])\n if (host.exists && !existing.exists && !roleRefusal(host, request.uid) && !planRefusal(request.org)) {\n const cover = await coverFor(stockPhotos(), request, read.value.title)\n if (cover) {\n read.value.coverImage = cover.src\n if (cover.alt && !read.value.coverImageAlt) read.value.coverImageAlt = cover.alt\n }\n }\n }\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(eventRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n if (existing.exists) {\n // Asked again under the same id: the draft it already wrote — unless\n // a person deleted it since, which a replay never brings back.\n return isDeleted(existing.data())\n ? { ok: false, status: 409, error: 'That event was deleted on the Events page' }\n : { ok: true, replayed: true, ...recordOf(existing) }\n }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n // The Events page's publish: `status`, and only for the publishing role.\n const published = hostRoleCanPublish((host.get('memberRoles') ?? {})[request.uid])\n const write = eventWrite({ ...read.value, status: published ? 'published' : 'draft' })\n tx.create(eventRef, {\n ...write.fields,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: write.fields.title,\n versionId: null,\n facts: {\n status: write.fields.status,\n startsAtMs: write.fields.startsAtMs,\n endsAtMs: write.fields.endsAtMs,\n ...(write.fields.coverImage ? { coverImage: write.fields.coverImage } : {}),\n },\n ...liveOf(published),\n }\n })\n },\n }\n}\n\nexport const eventDraftWriter = createEventDraftWriter()\n"],"names":["hostRoleCanPublish","hostRoleCanWrite","checkEntitlement","stockPhotoPicker","firebaseAdmin","checkEventDraftContent","EVENT_DRAFT_COVER_ADDRESS","eventDraftWallText","readEventDraftContent","EVENT_COVER_ALT_MAX_LENGTH","eventWrite","EVENT_DRAFT_ROLE_REFUSAL","EVENT_DRAFT_PLAN_REFUSAL","EVENT_DRAFT_UNPUBLISHED_NOTICE","eventDraftPastRefusal","wallText","isDeleted","data","Boolean","liveOf","published","live","notice","recordOf","event","id","name","String","versionId","facts","status","startsAtMs","Number","endsAtMs","coverImage","roleRefusal","host","uid","role","get","error","planRefusal","org","coverFor","picker","request","title","pick","hostId","subject","act","business","orientation","scope","seed","test","url","alt","replace","trim","slice","src","console","warn","createEventDraftWriter","deps","firestore","app","stockPhotos","refusal","context","collection","doc","exists","check","content","read","write","asked","ok","problems","value","now","getTime","timezone","db","hostRef","eventRef","existing","Promise","all","cover","coverImageAlt","runTransaction","tx","replayed","early","create","fields","createdAt","updatedAt","createdBy","eventDraftWriter"],"mappings":"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"}
|