@remit/web-client 0.0.230 → 0.0.232

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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/components/calendar/CalendarComposePane.stories.tsx +115 -0
  3. package/src/components/calendar/CalendarComposePane.tsx +94 -0
  4. package/src/components/calendar/CalendarComposeSeed.tsx +35 -0
  5. package/src/components/calendar/CalendarEventPane.stories.tsx +110 -0
  6. package/src/components/calendar/CalendarEventPane.tsx +90 -0
  7. package/src/components/calendar/CalendarShell.tsx +83 -0
  8. package/src/components/calendar/CalendarViewPlaceholder.tsx +37 -0
  9. package/src/components/calendar/CalendarWorkspace.stories.tsx +219 -0
  10. package/src/components/calendar/CalendarWorkspace.tsx +192 -0
  11. package/src/components/calendar/OpenCalendarEvent.tsx +221 -0
  12. package/src/components/calendar/WriteCalendarEvent.tsx +94 -0
  13. package/src/components/calendar/calendar-workspace.render.test.ts +193 -0
  14. package/src/components/calendar/calendar-write-flow.render.test.ts +356 -0
  15. package/src/components/mail/MailSidebarAdapter.tsx +17 -0
  16. package/src/hooks/calendar/calendar-reads.render.test.ts +265 -0
  17. package/src/hooks/calendar/calendar-writes.render.test.ts +305 -0
  18. package/src/hooks/calendar/draft.test.ts +241 -0
  19. package/src/hooks/calendar/draft.ts +168 -0
  20. package/src/hooks/calendar/index.ts +59 -0
  21. package/src/hooks/calendar/instance.ts +136 -0
  22. package/src/hooks/calendar/recurrence-rule.test.ts +92 -0
  23. package/src/hooks/calendar/recurrence-rule.ts +133 -0
  24. package/src/hooks/calendar/resource.test.ts +98 -0
  25. package/src/hooks/calendar/resource.ts +74 -0
  26. package/src/hooks/calendar/useCalendarEvent.ts +46 -0
  27. package/src/hooks/calendar/useCalendarEvents.ts +124 -0
  28. package/src/hooks/calendar/useCalendarWrites.ts +174 -0
  29. package/src/hooks/calendar/useCalendars.ts +43 -0
  30. package/src/hooks/calendar/window.test.ts +148 -0
  31. package/src/hooks/calendar/window.ts +163 -0
  32. package/src/hooks/useCalendarData.ts +120 -0
  33. package/src/lib/calendar-density.ts +34 -0
  34. package/src/lib/calendar-format.ts +29 -0
  35. package/src/lib/calendar-route.test.ts +210 -0
  36. package/src/lib/calendar-route.ts +187 -0
  37. package/src/lib/search-params.test.ts +77 -0
  38. package/src/lib/search-params.ts +59 -0
  39. package/src/routeTree.gen.ts +199 -0
  40. package/src/router.tsx +5 -0
  41. package/src/routes/calendar/$view.$date/$calendarObjectId/$recurrenceId.tsx +26 -0
  42. package/src/routes/calendar/$view.$date/$calendarObjectId/index.tsx +18 -0
  43. package/src/routes/calendar/$view.$date/$calendarObjectId.tsx +15 -0
  44. package/src/routes/calendar/$view.$date/index.tsx +25 -0
  45. package/src/routes/calendar/$view.$date/new.tsx +20 -0
  46. package/src/routes/calendar/$view.$date.tsx +128 -0
  47. package/src/routes/calendar/-calendar-tree.test.ts +267 -0
  48. package/src/routes/calendar/index.tsx +27 -0
  49. package/src/routes/calendar.tsx +98 -0
  50. package/src/routing/calendar-fragment.test.ts +66 -0
  51. package/src/routing/calendar.ts +150 -0
  52. package/src/routing/fragment.ts +19 -0
  53. package/src/routing/index.ts +10 -0
  54. package/src/routing/nav-selection.ts +2 -0
  55. package/src/test-support/http.ts +8 -0
@@ -0,0 +1,150 @@
1
+ import type { CalendarViewId } from "@remit/ui";
2
+ import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
3
+ import { useCallback, useMemo } from "react";
4
+ import {
5
+ canonicalCalendarParams,
6
+ isoDate,
7
+ stepCalendarDate,
8
+ } from "@/lib/calendar-route";
9
+ import { useRetainOpenPanels } from "./fragment";
10
+
11
+ const VIEW_ROUTE = "/calendar/$view/$date" as const;
12
+
13
+ /** Which zoom the calendar is at, on which day, over which calendars. */
14
+ export interface CalendarAddress {
15
+ view: CalendarViewId;
16
+ /** `YYYY-MM-DD`. */
17
+ date: string;
18
+ /** The calendars ticked in the query; empty means every one of them. */
19
+ calendarIds: readonly string[];
20
+ }
21
+
22
+ /**
23
+ * The calendar the address names.
24
+ *
25
+ * The route canonicalises its own segments before anything mounts, so what
26
+ * arrives here is always a view the ladder has and a day the calendar has —
27
+ * the fallbacks below only cover the frame before the router has resolved
28
+ * the params.
29
+ */
30
+ export function useCalendarAddress(): CalendarAddress {
31
+ const params = useParams({ from: VIEW_ROUTE, shouldThrow: false });
32
+ const search = useSearch({ from: VIEW_ROUTE, shouldThrow: false });
33
+ const view = params?.view ?? "";
34
+ const date = params?.date ?? "";
35
+ const calendarIds = search?.calendarId;
36
+ return useMemo(() => {
37
+ const canonical = canonicalCalendarParams(
38
+ { view, date },
39
+ isoDate(new Date()),
40
+ );
41
+ return { ...canonical, calendarIds: calendarIds ?? [] };
42
+ }, [view, date, calendarIds]);
43
+ }
44
+
45
+ /** The event the reading pane has open, and the instance of it, if any. */
46
+ export interface OpenCalendarEvent {
47
+ calendarObjectId: string;
48
+ /** One instance of a series; absent on the series itself. */
49
+ recurrenceId: string | undefined;
50
+ }
51
+
52
+ export function useOpenCalendarEvent(): OpenCalendarEvent | undefined {
53
+ const event = useParams({
54
+ from: "/calendar/$view/$date/$calendarObjectId",
55
+ shouldThrow: false,
56
+ });
57
+ const instance = useParams({
58
+ from: "/calendar/$view/$date/$calendarObjectId/$recurrenceId",
59
+ shouldThrow: false,
60
+ });
61
+ const calendarObjectId = event?.calendarObjectId;
62
+ const recurrenceId = instance?.recurrenceId;
63
+ return useMemo(
64
+ () => (calendarObjectId ? { calendarObjectId, recurrenceId } : undefined),
65
+ [calendarObjectId, recurrenceId],
66
+ );
67
+ }
68
+
69
+ /** Whether the composer is up, which is a route rather than a flag. */
70
+ export function useIsWritingEvent(): boolean {
71
+ const composer = useParams({
72
+ from: "/calendar/$view/$date/new",
73
+ shouldThrow: false,
74
+ });
75
+ return composer !== undefined;
76
+ }
77
+
78
+ export interface CalendarNavigation {
79
+ /** Change zoom, keeping the day on screen. */
80
+ goToView: (view: CalendarViewId) => void;
81
+ /** The previous or next screenful, measured in the view's own unit. */
82
+ step: (direction: -1 | 1) => void;
83
+ /** Home, from every view and every distance away. */
84
+ goToToday: () => void;
85
+ /**
86
+ * One occurrence names the series it belongs to and the instance under it;
87
+ * a resource that does not recur names only itself.
88
+ */
89
+ openEvent: (calendarObjectId: string, recurrenceId?: string) => void;
90
+ closeEvent: () => void;
91
+ openComposer: () => void;
92
+ }
93
+
94
+ /**
95
+ * Every move the calendar makes, as an address.
96
+ *
97
+ * Prev, next, today and the zoom switch are navigations rather than state
98
+ * changes, so the address always names what is on screen and a link to it
99
+ * reopens exactly that. Each one carries the open panes across the way the nav
100
+ * does, so the reader keeps their chrome moving through the week.
101
+ */
102
+ export function useCalendarNavigation(): CalendarNavigation {
103
+ const navigate = useNavigate();
104
+ const retainPanels = useRetainOpenPanels();
105
+ const { view, date } = useCalendarAddress();
106
+
107
+ const goTo = useCallback(
108
+ (params: { view: CalendarViewId; date: string }) => {
109
+ navigate({
110
+ to: VIEW_ROUTE,
111
+ params,
112
+ search: true,
113
+ hash: retainPanels,
114
+ });
115
+ },
116
+ [navigate, retainPanels],
117
+ );
118
+
119
+ return useMemo(
120
+ () => ({
121
+ goToView: (next: CalendarViewId) => goTo({ view: next, date }),
122
+ step: (direction: -1 | 1) =>
123
+ goTo({ view, date: stepCalendarDate(date, view, direction) }),
124
+ goToToday: () => goTo({ view, date: isoDate(new Date()) }),
125
+ openEvent: (calendarObjectId: string, recurrenceId?: string) =>
126
+ recurrenceId === undefined || recurrenceId === ""
127
+ ? navigate({
128
+ to: "/calendar/$view/$date/$calendarObjectId",
129
+ params: { view, date, calendarObjectId },
130
+ search: true,
131
+ hash: retainPanels,
132
+ })
133
+ : navigate({
134
+ to: "/calendar/$view/$date/$calendarObjectId/$recurrenceId",
135
+ params: { view, date, calendarObjectId, recurrenceId },
136
+ search: true,
137
+ hash: retainPanels,
138
+ }),
139
+ closeEvent: () => goTo({ view, date }),
140
+ openComposer: () =>
141
+ navigate({
142
+ to: "/calendar/$view/$date/new",
143
+ params: { view, date },
144
+ search: true,
145
+ hash: retainPanels,
146
+ }),
147
+ }),
148
+ [goTo, navigate, retainPanels, view, date],
149
+ );
150
+ }
@@ -27,6 +27,25 @@ export type OverlayPanel = (typeof overlayPanels)[number];
27
27
  export const isOverlayPanel = (panel: PanelFragment): panel is OverlayPanel =>
28
28
  (overlayPanels as readonly PanelFragment[]).includes(panel);
29
29
 
30
+ /**
31
+ * The set an overlay opening or closing leaves behind it.
32
+ *
33
+ * A write states the whole set, so raising the nav slide-over has to say what
34
+ * else is up or it takes it down. The panes stay and the previous overlay goes,
35
+ * because two overlays cannot both be up — which is how a reader crossing from
36
+ * their mail into the calendar and reaching for the folders keeps the rail they
37
+ * arrived with.
38
+ */
39
+ export function panelsWithOverlay(
40
+ open: readonly PanelFragment[],
41
+ overlay: OverlayPanel | undefined,
42
+ ): PanelFragment[] {
43
+ return [
44
+ ...open.filter((panel) => !isOverlayPanel(panel)),
45
+ ...(overlay ? [overlay] : []),
46
+ ];
47
+ }
48
+
30
49
  const panelFragmentSchema = z.enum(panelFragments);
31
50
 
32
51
  /**
@@ -3,6 +3,15 @@ export {
3
3
  useBrowsedList,
4
4
  useOpensDetail,
5
5
  } from "./browsed-list";
6
+ export {
7
+ type CalendarAddress,
8
+ type CalendarNavigation,
9
+ type OpenCalendarEvent,
10
+ useCalendarAddress,
11
+ useCalendarNavigation,
12
+ useIsWritingEvent,
13
+ useOpenCalendarEvent,
14
+ } from "./calendar";
6
15
  export {
7
16
  useAdoptComposeDraft,
8
17
  useCloseCompose,
@@ -18,6 +27,7 @@ export {
18
27
  overlayPanels,
19
28
  type PanelFragment,
20
29
  panelFragments,
30
+ panelsWithOverlay,
21
31
  parseOpenPanels,
22
32
  retainOpenPanelsAtTier,
23
33
  useOpenPanels,
@@ -5,6 +5,7 @@ import { locationIsOnList } from "@/lib/mail-route";
5
5
  * The nav entry the address selects, or `""` where none does.
6
6
  *
7
7
  * - `/settings/*` → "settings"
8
+ * - `/calendar/*` → "calendar"
8
9
  * - `/mail/outbox`, `/mail/flagged`, `/mail/brief` → that list
9
10
  * - `/mail/$mailboxId` → the mailbox id, which is what the sidebar keys on
10
11
  *
@@ -17,6 +18,7 @@ export function useSelectedNavId(): string {
17
18
  const mailbox = useParams({ from: "/mail/$mailboxId", shouldThrow: false });
18
19
 
19
20
  if (locationIsOnList(pathname, "/settings")) return "settings";
21
+ if (locationIsOnList(pathname, "/calendar")) return "calendar";
20
22
  if (locationIsOnList(pathname, "/mail/outbox")) return "outbox";
21
23
  if (locationIsOnList(pathname, "/mail/flagged")) return "flagged";
22
24
  if (locationIsOnList(pathname, "/mail/brief")) return "brief";
@@ -8,6 +8,8 @@ export interface HttpCall {
8
8
  method: string;
9
9
  url: string;
10
10
  path: string;
11
+ /** Every request header, lower-cased — a conditional write is one of these. */
12
+ headers: Record<string, string>;
11
13
  body: Record<string, unknown> | undefined;
12
14
  }
13
15
 
@@ -36,10 +38,16 @@ export const mockFetch = (responder: Responder = () => ({})): HttpMock => {
36
38
  const request = input instanceof Request ? input : undefined;
37
39
  const url = request ? request.url : String(input);
38
40
  const rawBody = request ? await request.clone().text() : undefined;
41
+ const headers: Record<string, string> = {};
42
+ const sent = request ? request.headers : new Headers(init?.headers);
43
+ sent.forEach((value, name) => {
44
+ headers[name.toLowerCase()] = value;
45
+ });
39
46
  const call: HttpCall = {
40
47
  method: (request?.method ?? init?.method ?? "GET").toUpperCase(),
41
48
  url,
42
49
  path: new URL(url, "http://localhost").pathname,
50
+ headers,
43
51
  body: rawBody ? JSON.parse(rawBody) : undefined,
44
52
  };
45
53
  calls.push(call);