@tetherto/dev-websites-core 0.3.2 → 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.
Files changed (35) hide show
  1. package/README.md +74 -0
  2. package/dist/cms/google-oauth.d.mts +20 -0
  3. package/dist/cms/google-oauth.d.mts.map +1 -0
  4. package/dist/cms/google-oauth.mjs +130 -0
  5. package/dist/cms/google-oauth.mjs.map +1 -0
  6. package/dist/cms/index.d.mts +2 -2
  7. package/dist/cms/index.mjs +2 -2
  8. package/dist/cms.mjs +436 -7
  9. package/dist/cms.mjs.map +1 -1
  10. package/dist/index.d.mts +24 -3
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +4 -3
  13. package/dist/index5.d.mts +2 -2
  14. package/dist/seo/index.mjs +2 -1
  15. package/dist/seo.mjs +2 -13
  16. package/dist/seo.mjs.map +1 -1
  17. package/dist/site-url.mjs +16 -0
  18. package/dist/site-url.mjs.map +1 -0
  19. package/dist/ui/components/Badge/Badge.d.mts +1 -1
  20. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts +5 -0
  21. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts.map +1 -0
  22. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs +72 -0
  23. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs.map +1 -0
  24. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts +6 -0
  25. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts.map +1 -0
  26. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs +7 -0
  27. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs.map +1 -0
  28. package/dist/ui/components/GoogleOAuthButton/index.d.mts +2 -0
  29. package/dist/ui/components/GoogleOAuthButton/index.mjs +2 -0
  30. package/dist/ui/index.d.mts +3 -1
  31. package/dist/ui/index.mjs +1 -0
  32. package/package.json +18 -2
  33. package/src/ui/components/GoogleOAuthButton/GoogleOAuthButton.tsx +69 -0
  34. package/src/ui/components/GoogleOAuthButton/googleOAuthStartPath.ts +2 -0
  35. package/src/ui/components/GoogleOAuthButton/index.ts +1 -0
package/dist/cms.mjs CHANGED
@@ -708,6 +708,31 @@ const showcaseContentBlocks = [
708
708
  imageBlock
709
709
  ];
710
710
  //#endregion
711
+ //#region src/cms/hooks/coerce-blog-post-categories.ts
712
+ function toCategoryIdList(value) {
713
+ if (value == null || value === "") return [];
714
+ return (Array.isArray(value) ? value : [value]).map((item) => {
715
+ if (typeof item === "string" || typeof item === "number") return String(item);
716
+ if (item && typeof item === "object" && "id" in item && item.id != null) return String(item.id);
717
+ return null;
718
+ }).filter((id) => Boolean(id));
719
+ }
720
+ /** Posts saved before `category` became `hasMany` still store a single value. */
721
+ const coerceBlogPostCategoriesOnWrite = ({ data }) => {
722
+ if (!data || !("category" in data)) return data;
723
+ data.category = toCategoryIdList(data.category);
724
+ return data;
725
+ };
726
+ const coerceBlogPostCategoriesOnRead = ({ doc }) => {
727
+ if (!doc) return doc;
728
+ if (doc.category == null || doc.category === "") {
729
+ doc.category = [];
730
+ return doc;
731
+ }
732
+ if (!Array.isArray(doc.category)) doc.category = [doc.category];
733
+ return doc;
734
+ };
735
+ //#endregion
711
736
  //#region src/cms/hooks/set-published-at.ts
712
737
  const setPublishedAt = ({ data }) => {
713
738
  if (data && !data.publishedAt && data._status === "published") data.publishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -746,7 +771,10 @@ function createBlogPosts(access) {
746
771
  delete: access.adminOnly
747
772
  },
748
773
  versions: draftsWithAutosave,
749
- hooks: { beforeChange: [setPublishedAt] },
774
+ hooks: {
775
+ beforeChange: [setPublishedAt, coerceBlogPostCategoriesOnWrite],
776
+ afterRead: [coerceBlogPostCategoriesOnRead]
777
+ },
750
778
  fields: [
751
779
  {
752
780
  name: "title",
@@ -798,6 +826,8 @@ function createBlogPosts(access) {
798
826
  name: "category",
799
827
  type: "relationship",
800
828
  relationTo: "categories",
829
+ hasMany: true,
830
+ label: "Categories",
801
831
  admin: { position: "sidebar" }
802
832
  },
803
833
  {
@@ -1048,6 +1078,300 @@ function createChangelogTags(access) {
1048
1078
  };
1049
1079
  }
1050
1080
  //#endregion
1081
+ //#region src/cms/collections/createEventTags.ts
1082
+ /** Stock event-tags (no site revalidation — add via overrides.hooks). */
1083
+ function createEventTags(access) {
1084
+ return {
1085
+ slug: "event-tags",
1086
+ labels: {
1087
+ singular: "Event Tag",
1088
+ plural: "Event Tags"
1089
+ },
1090
+ defaultSort: "order",
1091
+ admin: {
1092
+ useAsTitle: "name",
1093
+ defaultColumns: [
1094
+ "name",
1095
+ "slug",
1096
+ "order"
1097
+ ],
1098
+ description: "Reusable event categories (Meetup, Workshop, Hackathon, …). Editors can add types without code changes. Related from Events."
1099
+ },
1100
+ access: {
1101
+ read: access.publicRead,
1102
+ create: access.adminOnly,
1103
+ update: access.adminOnly,
1104
+ delete: access.adminOnly
1105
+ },
1106
+ fields: [
1107
+ {
1108
+ name: "name",
1109
+ type: "text",
1110
+ required: true,
1111
+ localized: true
1112
+ },
1113
+ {
1114
+ name: "slug",
1115
+ type: "text",
1116
+ required: true,
1117
+ unique: true,
1118
+ admin: {
1119
+ position: "sidebar",
1120
+ description: "URL-friendly identifier used by filters and relationships."
1121
+ },
1122
+ hooks: { beforeValidate: [slugify] }
1123
+ },
1124
+ {
1125
+ name: "order",
1126
+ type: "number",
1127
+ defaultValue: 0,
1128
+ admin: {
1129
+ position: "sidebar",
1130
+ description: "Display order in admin lists (ascending). Ties broken by name."
1131
+ }
1132
+ },
1133
+ {
1134
+ name: "description",
1135
+ type: "textarea",
1136
+ localized: true,
1137
+ admin: { description: "Optional context shown to editors." }
1138
+ }
1139
+ ]
1140
+ };
1141
+ }
1142
+ //#endregion
1143
+ //#region src/cms/collections/createEvents.ts
1144
+ /**
1145
+ * Display abbreviations accepted in addition to IANA names (e.g. Europe/Paris).
1146
+ * Sites map these to IANA when they need a timezone database identifier.
1147
+ */
1148
+ const TIMEZONE_ABBREVIATIONS = /* @__PURE__ */ new Set([
1149
+ "UTC",
1150
+ "GMT",
1151
+ "CET",
1152
+ "CEST",
1153
+ "WET",
1154
+ "WEST",
1155
+ "EET",
1156
+ "EEST",
1157
+ "BST",
1158
+ "EST",
1159
+ "EDT",
1160
+ "CST",
1161
+ "CDT",
1162
+ "MST",
1163
+ "MDT",
1164
+ "PST",
1165
+ "PDT"
1166
+ ]);
1167
+ function parseDate(value) {
1168
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
1169
+ if (typeof value === "string" && value.trim()) {
1170
+ const date = new Date(value);
1171
+ return Number.isNaN(date.getTime()) ? null : date;
1172
+ }
1173
+ return null;
1174
+ }
1175
+ function isValidIanaTimeZone(timeZone) {
1176
+ try {
1177
+ Intl.DateTimeFormat("en-US", { timeZone }).format(/* @__PURE__ */ new Date());
1178
+ return true;
1179
+ } catch {
1180
+ return false;
1181
+ }
1182
+ }
1183
+ const validateTimezone = (value) => {
1184
+ if (!value || !value.trim()) return "Timezone is required.";
1185
+ const timeZone = value.trim();
1186
+ if (TIMEZONE_ABBREVIATIONS.has(timeZone.toUpperCase())) return true;
1187
+ if (isValidIanaTimeZone(timeZone)) return true;
1188
+ return "Must be a timezone abbreviation (CET, UTC, …) or a valid IANA timezone (e.g. Europe/Paris).";
1189
+ };
1190
+ const validateExternalUrl = (value) => {
1191
+ if (!value || !value.trim()) return "External URL is required.";
1192
+ try {
1193
+ new URL(value);
1194
+ return true;
1195
+ } catch {
1196
+ return "Must be a valid URL (e.g. https://example.com/event).";
1197
+ }
1198
+ };
1199
+ const validateEndsAt = (value, { siblingData }) => {
1200
+ const end = parseDate(value);
1201
+ if (!end) return "End date and time is required.";
1202
+ const start = parseDate(siblingData && typeof siblingData === "object" && "startsAt" in siblingData ? siblingData.startsAt : void 0);
1203
+ if (start && end.getTime() <= start.getTime()) return "End date and time must be after the start.";
1204
+ return true;
1205
+ };
1206
+ /**
1207
+ * Stock events collection (shared fields only).
1208
+ * SEO / site-specific fields — add via `overrides.fields`.
1209
+ * Revalidation — add via `overrides.hooks`.
1210
+ */
1211
+ function createEvents(access) {
1212
+ return {
1213
+ slug: "events",
1214
+ labels: {
1215
+ singular: "Event",
1216
+ plural: "Events"
1217
+ },
1218
+ defaultSort: "startsAt",
1219
+ admin: {
1220
+ useAsTitle: "title",
1221
+ defaultColumns: [
1222
+ "title",
1223
+ "slug",
1224
+ "tag",
1225
+ "startsAt",
1226
+ "endsAt",
1227
+ "_status"
1228
+ ],
1229
+ description: "Reusable events consumed by marketing / community surfaces across sites."
1230
+ },
1231
+ access: {
1232
+ read: access.adminOrPublished,
1233
+ create: access.adminOnly,
1234
+ update: access.adminOnly,
1235
+ delete: access.adminOnly
1236
+ },
1237
+ versions: draftsWithAutosave,
1238
+ hooks: { beforeChange: [setPublishedAt] },
1239
+ fields: [
1240
+ {
1241
+ name: "title",
1242
+ type: "text",
1243
+ required: true,
1244
+ localized: true
1245
+ },
1246
+ {
1247
+ name: "slug",
1248
+ type: "text",
1249
+ required: true,
1250
+ unique: true,
1251
+ admin: {
1252
+ position: "sidebar",
1253
+ description: "URL-friendly identifier. Not localized — one slug per event."
1254
+ },
1255
+ hooks: { beforeValidate: [slugify] }
1256
+ },
1257
+ {
1258
+ name: "summary",
1259
+ type: "textarea",
1260
+ required: true,
1261
+ localized: true,
1262
+ admin: { description: "Short description for listings, previews, and calendar bodies." }
1263
+ },
1264
+ {
1265
+ name: "coverImage",
1266
+ type: "upload",
1267
+ relationTo: "media",
1268
+ admin: { description: "Optional cover/hero image for detail or promotional surfaces." }
1269
+ },
1270
+ {
1271
+ name: "startsAt",
1272
+ type: "date",
1273
+ required: true,
1274
+ admin: {
1275
+ position: "sidebar",
1276
+ date: {
1277
+ pickerAppearance: "dayAndTime",
1278
+ displayFormat: "MMM d, yyyy h:mm a"
1279
+ },
1280
+ description: "Event start date and time. Sites derive upcoming vs past from this value."
1281
+ }
1282
+ },
1283
+ {
1284
+ name: "endsAt",
1285
+ type: "date",
1286
+ required: true,
1287
+ validate: validateEndsAt,
1288
+ admin: {
1289
+ position: "sidebar",
1290
+ date: {
1291
+ pickerAppearance: "dayAndTime",
1292
+ displayFormat: "MMM d, yyyy h:mm a"
1293
+ },
1294
+ description: "Event end date and time. Must be after the start."
1295
+ }
1296
+ },
1297
+ {
1298
+ name: "timezone",
1299
+ type: "text",
1300
+ required: true,
1301
+ validate: validateTimezone,
1302
+ admin: {
1303
+ position: "sidebar",
1304
+ description: "Display timezone: abbreviation (CET, WET, UTC) or IANA name (Europe/Paris)."
1305
+ }
1306
+ },
1307
+ {
1308
+ name: "location",
1309
+ type: "text",
1310
+ required: true,
1311
+ localized: true,
1312
+ admin: { description: "Venue or platform (e.g. Discord, Online, Lisbon)." }
1313
+ },
1314
+ {
1315
+ name: "tag",
1316
+ type: "relationship",
1317
+ relationTo: "event-tags",
1318
+ required: true,
1319
+ admin: {
1320
+ position: "sidebar",
1321
+ description: "Primary event category from Event Tags."
1322
+ }
1323
+ },
1324
+ {
1325
+ name: "ctaKind",
1326
+ type: "select",
1327
+ required: true,
1328
+ defaultValue: "link",
1329
+ options: [{
1330
+ label: "External link",
1331
+ value: "link"
1332
+ }, {
1333
+ label: "Add to calendar",
1334
+ value: "calendar"
1335
+ }],
1336
+ admin: {
1337
+ position: "sidebar",
1338
+ description: "Primary listing action. Link opens the external URL; calendar lets the site offer an add-to-calendar flow."
1339
+ }
1340
+ },
1341
+ {
1342
+ name: "ctaLabel",
1343
+ type: "text",
1344
+ required: true,
1345
+ localized: true,
1346
+ admin: { description: "Label for the primary listing action (e.g. Register, Watch recording)." }
1347
+ },
1348
+ {
1349
+ name: "ctaUrl",
1350
+ type: "text",
1351
+ required: true,
1352
+ validate: validateExternalUrl,
1353
+ admin: { description: "External event URL. Opened for link actions; included in calendar details for calendar actions." }
1354
+ },
1355
+ {
1356
+ name: "content",
1357
+ type: "richText",
1358
+ localized: true,
1359
+ editor: richTextEditor(),
1360
+ admin: { description: "Optional long-form body for event detail pages." }
1361
+ },
1362
+ {
1363
+ name: "publishedAt",
1364
+ type: "date",
1365
+ admin: {
1366
+ position: "sidebar",
1367
+ date: { pickerAppearance: "dayAndTime" },
1368
+ description: "Auto-filled on first publish if left empty."
1369
+ }
1370
+ }
1371
+ ]
1372
+ };
1373
+ }
1374
+ //#endregion
1051
1375
  //#region src/cms/collections/createMedia.ts
1052
1376
  /** Stock media upload collection (always publicly readable once uploaded). */
1053
1377
  function createMedia(access) {
@@ -1174,7 +1498,7 @@ function createUsers(access) {
1174
1498
  return {
1175
1499
  slug: "users",
1176
1500
  admin: { useAsTitle: "email" },
1177
- auth: true,
1501
+ auth: { verify: false },
1178
1502
  access: {
1179
1503
  read: access.adminOnly,
1180
1504
  create: access.adminOnly,
@@ -1182,7 +1506,29 @@ function createUsers(access) {
1182
1506
  delete: access.adminOnly,
1183
1507
  admin: access.adminOnly
1184
1508
  },
1185
- fields: []
1509
+ fields: [
1510
+ {
1511
+ name: "googleId",
1512
+ type: "text",
1513
+ unique: true,
1514
+ index: true,
1515
+ admin: {
1516
+ position: "sidebar",
1517
+ readOnly: true,
1518
+ description: "Google OAuth user ID"
1519
+ }
1520
+ },
1521
+ {
1522
+ name: "name",
1523
+ type: "text",
1524
+ admin: { description: "Display name from Google" }
1525
+ },
1526
+ {
1527
+ name: "picture",
1528
+ type: "text",
1529
+ admin: { description: "Profile picture URL from Google" }
1530
+ }
1531
+ ]
1186
1532
  };
1187
1533
  }
1188
1534
  //#endregion
@@ -1313,7 +1659,9 @@ const STOCK_COLLECTION_SLUGS = [
1313
1659
  "pages",
1314
1660
  "legal-pages",
1315
1661
  "changelog-tags",
1316
- "changelog"
1662
+ "changelog",
1663
+ "event-tags",
1664
+ "events"
1317
1665
  ];
1318
1666
  //#endregion
1319
1667
  //#region src/cms/buildCmsConfig.ts
@@ -1341,13 +1689,20 @@ function buildCmsConfig(options) {
1341
1689
  "pages": createPages(access),
1342
1690
  "legal-pages": createLegalPages(access),
1343
1691
  "changelog-tags": createChangelogTags(access),
1344
- "changelog": createChangelog(access)
1692
+ "changelog": createChangelog(access),
1693
+ "event-tags": createEventTags(access),
1694
+ "events": createEvents(access)
1345
1695
  };
1346
1696
  const collections = [...STOCK_COLLECTION_SLUGS.map((slug) => applyCollectionOverride(stock[slug], overrides[slug])).filter((c) => c !== null), ...options.extraCollections ?? []];
1347
1697
  const siteSettings = applyGlobalOverride(createSiteSettings(access), overrides["site-settings"]);
1348
1698
  const globals = [...siteSettings ? [siteSettings] : [], ...options.extraGlobals ?? []];
1699
+ const { jobs: jobsOverride, ...payloadRest } = options.payload ?? {};
1349
1700
  return buildConfig({
1350
- ...options.payload,
1701
+ ...payloadRest,
1702
+ jobs: {
1703
+ autoRun: [{ cron: "* * * * *" }],
1704
+ ...jobsOverride
1705
+ },
1351
1706
  ...defined({
1352
1707
  sharp: options.sharp,
1353
1708
  cors: options.cors,
@@ -1414,6 +1769,80 @@ function createS3MediaPlugin(options = {}) {
1414
1769
  });
1415
1770
  }
1416
1771
  //#endregion
1417
- export { downloadCtaBlock as A, createAccessHelpers as B, leafBlocks as C, richTextEditor as D, getRichTextEditorConfig as E, languageOptions as F, createAuthors as I, seoFields as L, codeBlock as M, codeBlockLanguages as N, videoBlock as O, defaultCodeLanguage as P, slugify as R, accordionBlock as S, richTextBlock as T, setPublishedAt as _, applyCollectionOverride as a, showcaseContentBlocks as b, createUsers as c, createMedia as d, createChangelogTags as f, draftsWithAutosave as g, createBlogPosts as h, STOCK_COLLECTION_SLUGS as i, codeTabsBlock as j, imageBlock as k, createLegalPages as l, createCategories as m, requireEnvAtRuntime as n, applyGlobalOverride as o, createChangelog as p, buildCmsConfig as r, createSiteSettings as s, createS3MediaPlugin as t, createPages as u, blogContentBlocks as v, stepsBlock as w, sectionHeadingBlock as x, pageContentBlocks as y, createApiTokens as z };
1772
+ //#region src/cms/globals/createAnnouncementBanner.ts
1773
+ const validateBannerId = (value, { siblingData }) => {
1774
+ if (siblingData?.enabled && !String(value ?? "").trim()) return "Banner ID is required when the banner is enabled.";
1775
+ return true;
1776
+ };
1777
+ /** Announcement banner global (no site revalidation — add via applyGlobalOverride hooks). */
1778
+ function createAnnouncementBanner(access) {
1779
+ return {
1780
+ slug: "announcement-banner",
1781
+ label: "Announcement Banner",
1782
+ admin: { description: "Sticky site-wide banner shown above the main navigation. Bump Banner ID when replacing content so users who dismissed an older message in the current session see the new one." },
1783
+ access: {
1784
+ read: access.publicRead,
1785
+ update: access.adminOnly
1786
+ },
1787
+ fields: [
1788
+ {
1789
+ name: "enabled",
1790
+ type: "checkbox",
1791
+ defaultValue: false,
1792
+ admin: { description: "When off, the banner is hidden on the public site." }
1793
+ },
1794
+ {
1795
+ name: "bannerId",
1796
+ type: "text",
1797
+ admin: { description: "Stable identifier for this banner version. Change it when publishing new copy so users who dismissed an older banner in this session see the update." },
1798
+ validate: validateBannerId
1799
+ },
1800
+ {
1801
+ type: "row",
1802
+ fields: [{
1803
+ name: "prefix",
1804
+ type: "text",
1805
+ localized: true,
1806
+ maxLength: 8,
1807
+ admin: {
1808
+ width: "25%",
1809
+ description: "Accent label (max 8 characters), e.g. \"New:\""
1810
+ }
1811
+ }, {
1812
+ name: "description",
1813
+ type: "text",
1814
+ localized: true,
1815
+ maxLength: 1e3,
1816
+ admin: {
1817
+ width: "75%",
1818
+ description: "Banner message (max 1000 characters)."
1819
+ }
1820
+ }]
1821
+ },
1822
+ {
1823
+ type: "row",
1824
+ fields: [{
1825
+ name: "ctaLabel",
1826
+ type: "text",
1827
+ localized: true,
1828
+ maxLength: 18,
1829
+ admin: {
1830
+ width: "50%",
1831
+ description: "Optional button label (max 18 characters)."
1832
+ }
1833
+ }, {
1834
+ name: "ctaHref",
1835
+ type: "text",
1836
+ admin: {
1837
+ width: "50%",
1838
+ description: "Optional button URL. Relative paths stay in-site."
1839
+ }
1840
+ }]
1841
+ }
1842
+ ]
1843
+ };
1844
+ }
1845
+ //#endregion
1846
+ export { richTextBlock as A, languageOptions as B, blogContentBlocks as C, accordionBlock as D, sectionHeadingBlock as E, downloadCtaBlock as F, createAccessHelpers as G, seoFields as H, codeTabsBlock as I, codeBlock as L, richTextEditor as M, videoBlock as N, leafBlocks as O, imageBlock as P, codeBlockLanguages as R, coerceBlogPostCategoriesOnWrite as S, showcaseContentBlocks as T, slugify as U, createAuthors as V, createApiTokens as W, createCategories as _, STOCK_COLLECTION_SLUGS as a, setPublishedAt as b, createSiteSettings as c, createPages as d, createMedia as f, createChangelog as g, createChangelogTags as h, buildCmsConfig as i, getRichTextEditorConfig as j, stepsBlock as k, createUsers as l, createEventTags as m, createS3MediaPlugin as n, applyCollectionOverride as o, createEvents as p, requireEnvAtRuntime as r, applyGlobalOverride as s, createAnnouncementBanner as t, createLegalPages as u, createBlogPosts as v, pageContentBlocks as w, coerceBlogPostCategoriesOnRead as x, draftsWithAutosave as y, defaultCodeLanguage as z };
1418
1847
 
1419
1848
  //# sourceMappingURL=cms.mjs.map