@acms-subir/core 0.3.53 → 0.3.55

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/cli/main.mjs +2 -1
  3. package/cli/migrations.generated.mjs +76 -6
  4. package/docs/calendar.md +49 -2
  5. package/docs/google-calendar.md +302 -0
  6. package/docs/server.md +68 -1
  7. package/integration.mjs +15 -5
  8. package/package.json +1 -1
  9. package/src/components/LegalSettings.tsx +1 -1
  10. package/src/lib/api.ts +3 -0
  11. package/src/lib/backup/content.ts +4 -2
  12. package/src/lib/calendar/google/client.ts +167 -0
  13. package/src/lib/calendar/google/connect.ts +161 -0
  14. package/src/lib/calendar/google/context.ts +50 -0
  15. package/src/lib/calendar/google/inbound.ts +493 -0
  16. package/src/lib/calendar/google/links.ts +139 -0
  17. package/src/lib/calendar/google/map.ts +543 -0
  18. package/src/lib/calendar/google/match.ts +103 -0
  19. package/src/lib/calendar/google/migration.ts +71 -0
  20. package/src/lib/calendar/google/outbound.ts +272 -0
  21. package/src/lib/calendar/google/service.ts +193 -0
  22. package/src/lib/calendar/google/store.ts +241 -0
  23. package/src/lib/calendar/google/types.ts +188 -0
  24. package/src/lib/calendar/host-requests.ts +247 -0
  25. package/src/lib/calendar/member-form.ts +17 -11
  26. package/src/lib/calendar/member-words.ts +473 -0
  27. package/src/lib/calendar/members-config.ts +2 -2
  28. package/src/lib/calendar/moves.ts +81 -33
  29. package/src/lib/calendar/request-migration.ts +17 -0
  30. package/src/lib/calendar/request-store.ts +104 -0
  31. package/src/lib/calendar/requests.ts +24 -24
  32. package/src/lib/calendar/schedule.ts +8 -8
  33. package/src/lib/display/board.ts +3 -1
  34. package/src/lib/email/service.ts +22 -2
  35. package/src/lib/email/templates.ts +20 -2
  36. package/src/lib/legal/admin.ts +1 -1
  37. package/src/lib/legal/facts.ts +3 -0
  38. package/src/lib/legal/text-en.ts +4 -0
  39. package/src/lib/legal/text-es.ts +4 -0
  40. package/src/lib/live/crypto.ts +7 -28
  41. package/src/lib/repo/migrations.ts +8 -0
  42. package/src/lib/repo/sql.ts +25 -9
  43. package/src/lib/repo/types.ts +12 -3
  44. package/src/lib/scheduler.ts +3 -0
  45. package/src/lib/schema.ts +47 -0
  46. package/src/lib/sealed.ts +46 -0
  47. package/src/pages/admin/calendar/events/[id].astro +42 -1
  48. package/src/pages/admin/calendar/events/index.astro +82 -6
  49. package/src/pages/admin/calendar/google/match.astro +160 -0
  50. package/src/pages/admin/calendar/google.astro +107 -0
  51. package/src/pages/admin/calendar/settings.astro +273 -4
  52. package/src/pages/admin/index.astro +6 -2
  53. package/src/pages/admin/integrations.astro +6 -0
  54. package/src/pages/api/calendar/google/oauth/callback.ts +16 -0
  55. package/src/pages/members/_EventForm.astro +33 -27
  56. package/src/pages/members/_Layout.astro +25 -5
  57. package/src/pages/members/_http.ts +29 -6
  58. package/src/pages/members/events/[id].astro +21 -20
  59. package/src/pages/members/events/index.astro +295 -101
  60. package/src/pages/members/events/new.astro +11 -10
  61. package/src/server/index.ts +102 -0
  62. package/src/virtual.d.ts +1 -1
@@ -1,4 +1,10 @@
1
1
  import { ApiError } from '../../lib/api';
2
+ import type { AstroGlobal } from 'astro';
3
+ import type { Settings } from '../../lib/schema';
4
+ import { I18N, localePath, requestLocale } from '../../lib/i18n';
5
+ import { isExtraLocale } from '../../lib/translations';
6
+ import { siteLang, siteLocale } from '../../lib/dates';
7
+ import { memberError, memberStrings, type MemberStrings } from '../../lib/calendar/member-words';
2
8
 
3
9
  // Shared by the built-in "My events" pages: they're personal, so nothing
4
10
  // they answer (redirects included) may be cached or indexed.
@@ -12,14 +18,31 @@ export function keepPrivate(headers: Headers) {
12
18
  /** 303 after a POST (the browser GETs the list), 302 otherwise */
13
19
  export const redirectTo = (location: string, status = 303) => new Response(null, { status, headers: { Location: location, ...NO_STORE } });
14
20
 
15
- /** signed out: sign in, then come back here */
16
- export const signInRedirect = (url: URL) => redirectTo(`/admin/login?return=${encodeURIComponent(url.pathname + url.search)}`, 302);
21
+ /** signed out: sign in, then come back here (the address they used: /es/… is rewritten to these pages) */
22
+ export const signInRedirect = (url: URL, locals?: unknown) => {
23
+ const path = (locals as { acmsPath?: string } | undefined)?.acmsPath ?? url.pathname;
24
+ return redirectTo(`/admin/login?return=${encodeURIComponent(path + url.search)}`, 302);
25
+ };
26
+
27
+ /**
28
+ * The page's language (/es/members/events is these pages in Spanish): its
29
+ * words, links that keep the visitor in it, and the locale dates are written
30
+ * in (the site's own locale setting for the main language).
31
+ */
32
+ export function memberLanguage(Astro: Pick<AstroGlobal, 'url' | 'locals'>, settings: Pick<Settings, 'locale'>): { locale: string; lang: string; w: MemberStrings; here: (path: string) => string; dateLocale: string } {
33
+ const locale = I18N.enabled ? requestLocale(Astro.url, Astro.locals) : '';
34
+ const lang = locale || siteLang(settings);
35
+ return { locale, lang, w: memberStrings(lang), here: (path: string) => localePath(path, locale), dateLocale: isExtraLocale(locale) ? locale : siteLocale(settings) };
36
+ }
17
37
 
18
38
  export const notFound = () => new Response(null, { status: 404, statusText: 'Not found', headers: NO_STORE });
19
39
 
20
- /** an error from requests.ts → a message safe to show, and the status to answer with */
21
- export function failure(err: unknown): { message: string; status: number } {
22
- if (err instanceof ApiError) return { message: err.message, status: err.status };
40
+ /** an error from requests.ts (or moves.ts) → a message safe to show, in the page's words when given, and the status to answer with */
41
+ export function failure(err: unknown, w?: MemberStrings, lang?: string): { message: string; status: number; code: string } {
42
+ if (err instanceof ApiError) {
43
+ const code = String((err as { code?: unknown }).code ?? '');
44
+ return { message: w ? memberError(w, err, lang) : err.message, status: err.status, code };
45
+ }
23
46
  console.error(err);
24
- return { message: 'Something went wrong. Please try again.', status: 500 };
47
+ return { message: w?.somethingWrong ?? 'Something went wrong. Please try again.', status: 500, code: '' };
25
48
  }
@@ -3,22 +3,24 @@ import Layout from '../_Layout.astro';
3
3
  import EventForm from '../_EventForm.astro';
4
4
  import { getRepo } from '../../../lib/repo';
5
5
  import { currentUser } from '../../../server';
6
- import { ApiError } from '../../../lib/api';
6
+ import { coded } from '../../../lib/api';
7
7
  import { readForm } from '../../../lib/forms/http';
8
8
  import { MEMBERS, memberEventsPath, bodyToText, isHost, updateEventRequest, withdrawEventRequest } from '../../../lib/calendar/requests';
9
9
  import { eventFormValues, eventRequestInput, memberEventFields, readEventForm } from '../../../lib/calendar/member-form';
10
10
  import { mergeEventDraft } from '../../../lib/drafts';
11
- import { failure, keepPrivate, notFound, redirectTo, signInRedirect } from '../_http';
11
+ import { failure, keepPrivate, memberLanguage, notFound, redirectTo, signInRedirect } from '../_http';
12
+ import { fill } from '../../../lib/live/words';
12
13
 
13
14
  // Edit a request, or suggest changes to a live event the member hosts: the
14
15
  // form starts from the event with any changes already waiting merged in.
15
16
  // A live event stays as it is until the team approves (updateEventRequest).
16
17
  if (!MEMBERS.enabled) return notFound();
17
18
  const user = await currentUser(Astro);
18
- if (!user) return signInRedirect(Astro.url);
19
+ if (!user) return signInRedirect(Astro.url, Astro.locals);
19
20
  keepPrivate(Astro.response.headers);
20
21
  const repo = await getRepo(Astro.locals);
21
- const home = memberEventsPath();
22
+ const { w, lang, here } = memberLanguage(Astro, await repo.getSettings());
23
+ const home = here(memberEventsPath());
22
24
  const hooks = { url: Astro.url, locals: Astro.locals };
23
25
  const id = Astro.params.id ?? '';
24
26
  const event = await repo.getEvent(id);
@@ -40,12 +42,12 @@ if (Astro.request.method === 'POST') {
40
42
  await withdrawEventRequest(repo, user, id, hooks);
41
43
  return redirectTo(`${home}?withdrawn=1`);
42
44
  }
43
- if (op !== 'update') throw new ApiError(400, 'That action isn’t available here.');
45
+ if (op !== 'update') throw coded(400, 'unavailable', 'That action isn’t available here.');
44
46
  values = readEventForm(form, fields);
45
47
  await updateEventRequest(repo, user, id, eventRequestInput(values), hooks);
46
48
  return redirectTo(`${home}?${live ? 'changes' : 'updated'}=1`);
47
49
  } catch (err) {
48
- const f = failure(err);
50
+ const f = failure(err, w, lang);
49
51
  error = f.message;
50
52
  Astro.response.status = f.status;
51
53
  }
@@ -53,34 +55,33 @@ if (Astro.request.method === 'POST') {
53
55
 
54
56
  const r = event.request;
55
57
  const canRequest = user.can('events.request');
56
- const heading = live ? 'Suggest changes' : 'Edit your request';
57
- const lede = live
58
- ? `“${event.title}” stays as it is on the site until the team approves your changes.`
59
- : r?.state === 'declined' ? 'Make your changes and send it again.'
60
- : r ? 'It’s waiting for approval. Saving sends the updated request.'
61
- : 'It isn’t on the calendar yet. Send it to the team when it’s ready.';
58
+ const heading = live ? w.suggestTitle : w.editTitle;
59
+ const lede = live ? fill(w.suggestLede, { title: event.title })
60
+ : r?.state === 'declined' ? w.declinedLede
61
+ : r ? w.waitingLede
62
+ : w.draftLede;
62
63
  const waiting = live && r?.kind === 'changes' && r.state === 'pending';
63
64
  const reply = r?.state === 'declined' ? r.reply : '';
64
65
  const canWithdraw = canRequest && r && !(r.kind === 'changes' && r.state === 'declined');
65
66
  ---
66
67
 
67
- <Layout title={heading} user={user}>
68
+ <Layout title={heading} user={user} w={w}>
68
69
  <div>
69
- <a class="pc-mem-back" href={home}>← My events</a>
70
+ <a class="pc-mem-back" href={home}>{w.back}</a>
70
71
  <div class="pc-mem-head"><h1>{heading}</h1></div>
71
72
  <p class="pc-mem-lede">{lede}</p>
72
73
  </div>
73
- {waiting && <p class="pc-mem-reply">The changes you sent are waiting for approval; they’re filled in below. Sending again replaces them.</p>}
74
+ {waiting && <p class="pc-mem-reply">{w.changesWaitingNote}</p>}
74
75
  {r?.state === 'declined' && (
75
- <p class="pc-mem-reply"><strong>{r.kind === 'changes' ? 'The team declined your last changes.' : 'The team declined this request.'}</strong>{reply && <> They said: {reply}</>}</p>
76
+ <p class="pc-mem-reply"><strong>{r.kind === 'changes' ? w.declinedChanges : w.declinedRequest}</strong>{reply && <> {w.theySaid} {reply}</>}</p>
76
77
  )}
77
78
  {canRequest
78
- ? <EventForm values={values} calendars={calendars} fields={fields} op="update" submitLabel={live ? 'Send changes for approval' : 'Submit for approval'} error={error} />
79
- : <p class="pc-bk-error" role="alert">Your role doesn’t include “Request events”. Ask the site’s team if you need it.</p>}
79
+ ? <EventForm values={values} calendars={calendars} fields={fields} op="update" submitLabel={live ? w.sendChanges : w.submit} error={error} w={w} lang={lang} />
80
+ : <p class="pc-bk-error" role="alert">{w.noRequestRole}</p>}
80
81
  {canWithdraw && (
81
- <form method="post" onsubmit={r!.kind === 'new' ? "return confirm('Withdraw this request?')" : "return confirm('Withdraw these changes?')"}>
82
+ <form method="post" onsubmit={'return confirm(' + JSON.stringify(r!.kind === 'new' ? w.confirmWithdrawRequest : w.confirmWithdrawChanges) + ')'}>
82
83
  <input type="hidden" name="op" value="withdraw" />
83
- <button class="pc-mem-btn is-danger" type="submit">{r!.kind === 'new' ? 'Withdraw request' : 'Withdraw changes'}</button>
84
+ <button class="pc-mem-btn is-danger" type="submit">{r!.kind === 'new' ? w.withdrawRequest : w.withdrawChanges}</button>
84
85
  </form>
85
86
  )}
86
87
  </Layout>