diff --git a/packages/core/src/__mocks__/v1/events/events.misc.ts b/packages/core/src/__mocks__/v1/events/events.misc.ts deleted file mode 100644 index 49c6c64a4..000000000 --- a/packages/core/src/__mocks__/v1/events/events.misc.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ObjectId } from "bson"; -import { Origin } from "@core/constants/core.constants"; -import { type CompassEvent } from "@core/types/compass-event.contracts"; - -/** - * Assortment of events with no direct relation to another - */ - -const USER = new ObjectId().toString(); - -export const CHILL_ALL_DAY: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "6csjad336cs3ibb469imcb9kc9gj6bb26ss30bb56kojgoj464o66ohh60", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "chill all day", - description: "just chillin", - isAllDay: true, - startDate: "2022-09-23", - endDate: "2022-09-24", -}; -export const CLIMB: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "ccq34eb261j3ab9jckpj6b9kcos6cbb26pi38b9pc5i64e9mcgp3ao9p6o", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "Climb", - description: "", - isAllDay: false, - startDate: "2022-03-01T17:00:00-06:00", - endDate: "2022-03-01T19:00:00-06:00", -}; - -export const EUROPE_TRIP: CompassEvent = { - _id: new ObjectId().toString(), - user: USER, - origin: Origin.COMPASS, - title: "Europe Trip", - description: "Italy, Germany", - startDate: "2025-12-10T00:00:00-00:00", - endDate: "2025-12-10T00:00:00-00:00", - order: 2, -}; -export const LEARN_CHINESE: CompassEvent = { - _id: new ObjectId().toString(), - description: "", - origin: Origin.COMPASS, - title: "Learn Chinese", - user: USER, - startDate: "2025-06-09", - endDate: "2025-06-09", - order: 1, -}; -export const MARCH_1: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "2ip0l4k0kqhg22cagtmrlml5mn", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "Mar 1", - description: "", - isAllDay: true, - startDate: "2022-03-01", - endDate: "2022-03-02", -}; -export const GROCERIES: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "pihjll1k75s1g9019ru6tkb97c", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "groceries", - description: "foo", - isAllDay: false, - startDate: "2022-02-21T11:45:00-06:00", - endDate: "2022-02-21T12:45:00-06:00", -}; -export const MULTI_WEEK: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "1k2rgneltn79cbchccut08alqs", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "multiweek event", - description: "", - isAllDay: true, - startDate: "2022-09-01T00:00:00-06:00", - endDate: "2022-09-22T00:00:00-06:00", -}; -export const TY_TIM: CompassEvent = { - _id: new ObjectId().toString(), - gEventId: "726v6dgnasekgmv5hc1jifpumm", - user: USER, - origin: Origin.GOOGLE_IMPORT, - title: "Ty & Tim", - description: - "──────────\n\nTim S is inviting you to a scheduled Zoom meeting.\n\nJoin Zoom Meeting\nhttps://us02web.zoom.us/j/87324397243?pwd=ZmpuYitCelZYVll0aDdiVUNXejdzdz09\n\nMeeting ID: 873 2439 7243\nPasscode: 305275\nOne tap mobile\n+16465588656,,87324397243#,,,,*305275# US (New York)\n+13017158592,,87324397243#,,,,*305275# US (Washington DC)\n\nDial by your location\n +1 646 558 8656 US (New York)\n +1 301 715 8592 US (Washington DC)\n +1 312 626 6799 US (Chicago)\n +1 669 900 9128 US (San Jose)\n +1 253 215 8782 US (Tacoma)\n +1 346 248 7799 US (Houston)\nMeeting ID: 873 2439 7243\nPasscode: 305275\nFind your local number: https://us02web.zoom.us/u/kdbALZcOSa\n\n\n──────────", - isAllDay: false, - startDate: "2022-03-01T12:15:00-06:00", - endDate: "2022-03-01T12:45:00-06:00", -}; diff --git a/packages/web/declaration.d.ts b/packages/web/declaration.d.ts index 8fc8e681c..08181fb66 100644 --- a/packages/web/declaration.d.ts +++ b/packages/web/declaration.d.ts @@ -48,7 +48,7 @@ interface Window { clear: () => void; }; releaseNotesPrompt?: { - getState: () => import("@web/auth/state/release-notes-prompt.store").ReleaseNotesPromptState; + getState: () => import("@web/components/ReleaseNotesPrompt/release-notes-prompt.store").ReleaseNotesPromptState; open: () => void; close: () => void; }; diff --git a/packages/web/src/__tests__/__mocks__/server/mock.handlers.ts b/packages/web/src/__tests__/__mocks__/server/mock.handlers.ts index 24dd991de..e023b62be 100644 --- a/packages/web/src/__tests__/__mocks__/server/mock.handlers.ts +++ b/packages/web/src/__tests__/__mocks__/server/mock.handlers.ts @@ -1,28 +1,36 @@ import { faker } from "@faker-js/faker"; import { rest } from "msw"; -import { - CLIMB, - GROCERIES, - MARCH_1, - MULTI_WEEK, - TY_TIM, -} from "@core/__mocks__/v1/events/events.misc"; +import { Origin } from "@core/constants/core.constants"; import { Status } from "@core/errors/status.codes"; +import { createMockStandaloneEvent } from "@core/util/test/ccal.event.factory"; import { ENV_WEB } from "@web/common/constants/env.constants"; import { freshenEventStartEndDate } from "@web/views/Week/week-view.render.test.utils"; +const createGoogleImportEvent: typeof createMockStandaloneEvent = ( + overrides = {}, + allDayEvent, + dateDiff, +) => + createMockStandaloneEvent( + { origin: Origin.GOOGLE_IMPORT, ...overrides }, + allDayEvent, + dateDiff, + ); + export const globalHandlers = [ rest.get("http://localhost/version.json", (_req, res, ctx) => { return res(ctx.json({ version: "dev" })); }), rest.get(`${ENV_WEB.API_BASEURL}/event`, (_req, res, ctx) => { const events = [ - CLIMB, - MARCH_1, - MULTI_WEEK, - TY_TIM, - // TODO: Need some way to inject the event into globalHandlers.events in a more dynamic way. - freshenEventStartEndDate(GROCERIES), + createGoogleImportEvent(), + createGoogleImportEvent({}, true), + createGoogleImportEvent({ isAllDay: true }, true, { + value: 21, + unit: "days", + }), + createGoogleImportEvent(), + freshenEventStartEndDate(createGoogleImportEvent()), ]; return res(ctx.json(events)); }), diff --git a/packages/web/src/__tests__/helpers/web-test-seams.ts b/packages/web/src/__tests__/helpers/web-test-seams.ts index b339c169f..014b87cd6 100644 --- a/packages/web/src/__tests__/helpers/web-test-seams.ts +++ b/packages/web/src/__tests__/helpers/web-test-seams.ts @@ -9,7 +9,7 @@ import { registerUseStartGoogleAuthorizationForTests, resetUseStartGoogleAuthorizationForTests, type UseStartGoogleAuthorization, -} from "@web/auth/google/authorization/useStartGoogleAuthorization.registry"; +} from "@web/auth/google/authorization/useStartGoogleAuthorization"; import { resetGoogleAvailabilityForTests, setGoogleAvailabilityForTests, diff --git a/packages/web/src/__tests__/utils/factories/event.factory.ts b/packages/web/src/__tests__/utils/factories/event.factory.ts index 765ee07bf..307be8fde 100644 --- a/packages/web/src/__tests__/utils/factories/event.factory.ts +++ b/packages/web/src/__tests__/utils/factories/event.factory.ts @@ -5,8 +5,8 @@ import { EventIdSchema, } from "@core/types/domain-primitives"; import { type Event, EventScheduleSchema } from "@core/types/event.contracts"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; import { createObjectIdString } from "@web/common/utils/id/object-id.util"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; const DEFAULT_CALENDAR_ID: CalendarId = CalendarIdSchema.parse( createObjectIdString(), diff --git a/packages/web/src/__tests__/utils/web.test.util.ts b/packages/web/src/__tests__/utils/web.test.util.ts deleted file mode 100644 index 97d8a5c96..000000000 --- a/packages/web/src/__tests__/utils/web.test.util.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const arraysAreEqual = (a: unknown[], b: unknown[]) => { - return ( - Array.isArray(a) && - Array.isArray(b) && - a.length === b.length && - a.every((val, index) => val === b[index]) - ); -}; diff --git a/packages/web/src/calendars/calendar.api.ts b/packages/web/src/api/calendar.api.ts similarity index 100% rename from packages/web/src/calendars/calendar.api.ts rename to packages/web/src/api/calendar.api.ts diff --git a/packages/web/src/auth/compass/session/SessionProvider.test.tsx b/packages/web/src/auth/compass/session/SessionProvider.test.tsx index 6c9af9051..7162a4852 100644 --- a/packages/web/src/auth/compass/session/SessionProvider.test.tsx +++ b/packages/web/src/auth/compass/session/SessionProvider.test.tsx @@ -1,12 +1,12 @@ import { renderHook, waitFor } from "@testing-library/react"; import { act, useContext } from "react"; import { session } from "@web/auth/compass/session/Session"; +import { DEFAULT_AUTH_STATE } from "@web/auth/compass/state/auth.state.util"; import { initialUserMetadataState, userMetadataActions, useUserMetadataStore, } from "@web/auth/state/user-metadata.store"; -import { DEFAULT_AUTH_STATE } from "@web/common/constants/auth.constants"; import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; // Create mocks at module level diff --git a/packages/web/src/auth/compass/session/session.util.test.ts b/packages/web/src/auth/compass/session/session.util.test.ts index bdabc5d85..554f7fade 100644 --- a/packages/web/src/auth/compass/session/session.util.test.ts +++ b/packages/web/src/auth/compass/session/session.util.test.ts @@ -1,4 +1,4 @@ -import { UNAUTHENTICATED_USER } from "@web/common/constants/auth.constants"; +import { UNAUTHENTICATED_USER } from "@web/auth/compass/session/session.util"; import { session } from "./Session"; import { getUserId } from "./session.util"; import { beforeEach, describe, expect, it, spyOn } from "bun:test"; diff --git a/packages/web/src/auth/compass/session/session.util.ts b/packages/web/src/auth/compass/session/session.util.ts index 77bcd074b..e55209a84 100644 --- a/packages/web/src/auth/compass/session/session.util.ts +++ b/packages/web/src/auth/compass/session/session.util.ts @@ -1,5 +1,6 @@ import { session } from "@web/auth/compass/session/Session"; -import { UNAUTHENTICATED_USER } from "@web/common/constants/auth.constants"; + +export const UNAUTHENTICATED_USER = "UNAUTHENTICATED_USER"; interface AccessTokenPayload { sub: string; diff --git a/packages/web/src/auth/compass/state/auth.state.util.ts b/packages/web/src/auth/compass/state/auth.state.util.ts index 857dc8e02..5c1bd7404 100644 --- a/packages/web/src/auth/compass/state/auth.state.util.ts +++ b/packages/web/src/auth/compass/state/auth.state.util.ts @@ -1,12 +1,18 @@ -import { - type AuthState, - AuthStateSchema, - DEFAULT_AUTH_STATE, -} from "@web/common/constants/auth.constants"; +import { z } from "zod"; import { STORAGE_KEYS } from "@web/common/constants/storage.constants"; import { persistentBrowserStore } from "@web/common/storage/browser-key-value.store"; import { clearGoogleRevokedState } from "../../google/state/google.auth.state"; +export const AuthStateSchema = z.object({ + hasAuthenticated: z.boolean().default(false), + lastKnownEmail: z.string().email().optional(), + shouldPromptSignUpAfterAnonymousCalendarChange: z.boolean().default(false), +}); + +export type AuthState = z.infer; + +export const DEFAULT_AUTH_STATE: AuthState = AuthStateSchema.parse({}); + const authStateListeners = new Set<() => void>(); function emitAuthStateChange(): void { diff --git a/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.registry.ts b/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.registry.ts deleted file mode 100644 index 20a744809..000000000 --- a/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.registry.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useStartGoogleAuthorizationImpl } from "./useStartGoogleAuthorization.impl"; - -export type UseStartGoogleAuthorization = - typeof useStartGoogleAuthorizationImpl; - -let useStartGoogleAuthorizationHook: UseStartGoogleAuthorization = - useStartGoogleAuthorizationImpl; - -export function registerUseStartGoogleAuthorizationForTests( - hook: UseStartGoogleAuthorization, -): void { - useStartGoogleAuthorizationHook = hook; -} - -export function resetUseStartGoogleAuthorizationForTests(): void { - useStartGoogleAuthorizationHook = useStartGoogleAuthorizationImpl; -} - -export function useStartGoogleAuthorizationFromRegistry( - ...args: Parameters -): ReturnType { - return useStartGoogleAuthorizationHook(...args); -} diff --git a/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.ts b/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.ts index f5b781fde..a45645dc3 100644 --- a/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.ts +++ b/packages/web/src/auth/google/authorization/useStartGoogleAuthorization.ts @@ -1 +1,23 @@ -export { useStartGoogleAuthorizationFromRegistry as useStartGoogleAuthorization } from "./useStartGoogleAuthorization.registry"; +import { useStartGoogleAuthorizationImpl } from "./useStartGoogleAuthorization.impl"; + +export type UseStartGoogleAuthorization = + typeof useStartGoogleAuthorizationImpl; + +let useStartGoogleAuthorizationHook: UseStartGoogleAuthorization = + useStartGoogleAuthorizationImpl; + +export function registerUseStartGoogleAuthorizationForTests( + hook: UseStartGoogleAuthorization, +): void { + useStartGoogleAuthorizationHook = hook; +} + +export function resetUseStartGoogleAuthorizationForTests(): void { + useStartGoogleAuthorizationHook = useStartGoogleAuthorizationImpl; +} + +export function useStartGoogleAuthorization( + ...args: Parameters +): ReturnType { + return useStartGoogleAuthorizationHook(...args); +} diff --git a/packages/web/src/auth/google/google-auth-config.test.ts b/packages/web/src/auth/google/google-auth-config.test.ts new file mode 100644 index 000000000..5c699ab8e --- /dev/null +++ b/packages/web/src/auth/google/google-auth-config.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test"; + +process.env.PORT ??= "3000"; + +const { isGoogleAuthConfigured } = await import("./google-auth-config"); + +describe("isGoogleAuthConfigured", () => { + it("rejects missing or empty Google client IDs", () => { + expect(isGoogleAuthConfigured()).toBe(false); + expect(isGoogleAuthConfigured("")).toBe(false); + expect(isGoogleAuthConfigured("undefined")).toBe(false); + }); + + it("accepts a custom Google client ID", () => { + expect( + isGoogleAuthConfigured("1234567890-example.apps.googleusercontent.com"), + ).toBe(true); + }); +}); diff --git a/packages/web/src/auth/google/google-auth-config.ts b/packages/web/src/auth/google/google-auth-config.ts new file mode 100644 index 000000000..e9e9e2f6f --- /dev/null +++ b/packages/web/src/auth/google/google-auth-config.ts @@ -0,0 +1,8 @@ +import { ENV_WEB } from "@web/common/constants/env.constants"; + +export const isGoogleAuthConfigured = (clientId?: string): boolean => + Boolean(clientId && clientId !== "undefined"); + +export const IS_GOOGLE_AUTH_CONFIGURED = isGoogleAuthConfigured( + ENV_WEB.GOOGLE_CLIENT_ID, +); diff --git a/packages/web/src/auth/google/hooks/useIsGoogleAvailable/useIsGoogleAvailable.ts b/packages/web/src/auth/google/hooks/useIsGoogleAvailable/useIsGoogleAvailable.ts index 46f8270f2..17e905682 100644 --- a/packages/web/src/auth/google/hooks/useIsGoogleAvailable/useIsGoogleAvailable.ts +++ b/packages/web/src/auth/google/hooks/useIsGoogleAvailable/useIsGoogleAvailable.ts @@ -1,5 +1,5 @@ import { AppConfigApi } from "@web/api/app-config.api"; -import { IS_GOOGLE_AUTH_CONFIGURED } from "@web/common/constants/env.constants"; +import { IS_GOOGLE_AUTH_CONFIGURED } from "@web/auth/google/google-auth-config"; import { createGoogleAvailability } from "./useIsGoogleAvailable.factory"; const googleAvailability = createGoogleAvailability({ diff --git a/packages/web/src/auth/state/release-notes-prompt.store.ts b/packages/web/src/auth/state/release-notes-prompt.store.ts deleted file mode 100644 index c9bfaa409..000000000 --- a/packages/web/src/auth/state/release-notes-prompt.store.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { create } from "zustand"; -import { persistentBrowserStore } from "@web/common/storage/browser-key-value.store"; - -export interface ReleaseNotesPromptState { - isOpen: boolean; -} - -export const useReleaseNotesPromptStore = create()( - () => ({ isOpen: false }), -); - -const SCHEDULED_OPEN_AT_STORAGE_KEY = - "compass.onboarding.release-notes-prompt-scheduled-at"; - -let scheduleOpenTimeoutId: number | undefined; - -const clearScheduledOpenStorage = () => { - persistentBrowserStore.remove(SCHEDULED_OPEN_AT_STORAGE_KEY); -}; - -const clearScheduledOpen = () => { - if (scheduleOpenTimeoutId !== undefined) { - window.clearTimeout(scheduleOpenTimeoutId); - scheduleOpenTimeoutId = undefined; - } -}; - -const openPrompt = () => { - clearScheduledOpen(); - clearScheduledOpenStorage(); - useReleaseNotesPromptStore.setState({ isOpen: true }); -}; - -const startScheduledOpenTimer = (delayMs: number) => { - clearScheduledOpen(); - scheduleOpenTimeoutId = window.setTimeout(() => { - scheduleOpenTimeoutId = undefined; - openPrompt(); - }, delayMs); -}; - -const resumeScheduledOpen = () => { - if (!persistentBrowserStore.isAvailable()) { - return; - } - - const storedOpenAt = persistentBrowserStore.get( - SCHEDULED_OPEN_AT_STORAGE_KEY, - ); - if (storedOpenAt === null) { - return; - } - - const openAtMs = Number(storedOpenAt); - if (!Number.isFinite(openAtMs)) { - clearScheduledOpenStorage(); - return; - } - - const remainingMs = openAtMs - Date.now(); - if (remainingMs <= 0) { - openPrompt(); - return; - } - - startScheduledOpenTimer(remainingMs); -}; - -export const releaseNotesPromptActions = { - open: () => { - openPrompt(); - }, - close: () => { - clearScheduledOpen(); - clearScheduledOpenStorage(); - useReleaseNotesPromptStore.setState({ isOpen: false }); - }, - scheduleOpen: (delayMs = 45_000) => { - if (persistentBrowserStore.isAvailable()) { - persistentBrowserStore.set( - SCHEDULED_OPEN_AT_STORAGE_KEY, - String(Date.now() + delayMs), - ); - } - startScheduledOpenTimer(delayMs); - }, -}; - -export const selectReleaseNotesPromptOpen = (state: ReleaseNotesPromptState) => - state.isOpen; - -// Semantic bridge for e2e tests, mirroring the user-metadata store. Lets tests -// raise the post-signup prompt without completing a real (backend-dependent) -// signup. Merge (don't overwrite) so sibling stores' bridges survive. -if (typeof window !== "undefined") { - resumeScheduledOpen(); - - window.__COMPASS_E2E_STORE__ = { - ...window.__COMPASS_E2E_STORE__, - releaseNotesPrompt: { - getState: useReleaseNotesPromptStore.getState, - open: releaseNotesPromptActions.open, - close: releaseNotesPromptActions.close, - }, - }; -} diff --git a/packages/web/src/calendars/calendar.query.ts b/packages/web/src/calendars/calendar.query.ts index 180849623..d8bb992b7 100644 --- a/packages/web/src/calendars/calendar.query.ts +++ b/packages/web/src/calendars/calendar.query.ts @@ -1,7 +1,7 @@ import { queryOptions, useQuery } from "@tanstack/react-query"; import { type Calendar } from "@core/types/calendar.contracts"; +import { CalendarApi } from "@web/api/calendar.api"; import { useSession } from "@web/auth/compass/session/useSession"; -import { CalendarApi } from "@web/calendars/calendar.api"; import { getLocalCalendarSentinelId, synthesizeLocalCalendar, diff --git a/packages/web/src/calendars/useCalendarVisibility.ts b/packages/web/src/calendars/useCalendarVisibility.ts index 7476787ec..f8be9cc47 100644 --- a/packages/web/src/calendars/useCalendarVisibility.ts +++ b/packages/web/src/calendars/useCalendarVisibility.ts @@ -2,7 +2,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { type Calendar } from "@core/types/calendar.contracts"; import { type CalendarId } from "@core/types/domain-primitives"; -import { CalendarApi } from "@web/calendars/calendar.api"; +import { CalendarApi } from "@web/api/calendar.api"; import { calendarQueryKeys } from "@web/calendars/calendar.query"; import { showErrorToast } from "@web/common/utils/toast/error-toast.util"; import { removeEventsByCalendarFromQueries } from "@web/events/queries/event.query.cache"; diff --git a/packages/web/src/common/constants/auth.constants.ts b/packages/web/src/common/constants/auth.constants.ts deleted file mode 100644 index 5c845962a..000000000 --- a/packages/web/src/common/constants/auth.constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; - -export const AuthStateSchema = z.object({ - hasAuthenticated: z.boolean().default(false), - lastKnownEmail: z.string().email().optional(), - shouldPromptSignUpAfterAnonymousCalendarChange: z.boolean().default(false), -}); - -export type AuthState = z.infer; - -export const DEFAULT_AUTH_STATE: AuthState = { - hasAuthenticated: false, - shouldPromptSignUpAfterAnonymousCalendarChange: false, -}; - -export const UNAUTHENTICATED_USER = "UNAUTHENTICATED_USER"; diff --git a/packages/web/src/common/constants/env.constants.test.ts b/packages/web/src/common/constants/env.constants.test.ts index 99c3e9452..50d05a717 100644 --- a/packages/web/src/common/constants/env.constants.test.ts +++ b/packages/web/src/common/constants/env.constants.test.ts @@ -2,9 +2,7 @@ import { describe, expect, it } from "bun:test"; process.env.PORT = "3000"; -const { getApiBaseUrl, isGoogleAuthConfigured } = await import( - "./env.constants" -); +const { getApiBaseUrl } = await import("./env.constants"); describe("getApiBaseUrl", () => { it("defaults to the local backend API using the configured port", () => { @@ -23,17 +21,3 @@ describe("getApiBaseUrl", () => { ); }); }); - -describe("isGoogleAuthConfigured", () => { - it("rejects missing or empty Google client IDs", () => { - expect(isGoogleAuthConfigured()).toBe(false); - expect(isGoogleAuthConfigured("")).toBe(false); - expect(isGoogleAuthConfigured("undefined")).toBe(false); - }); - - it("accepts a custom Google client ID", () => { - expect( - isGoogleAuthConfigured("1234567890-example.apps.googleusercontent.com"), - ).toBe(true); - }); -}); diff --git a/packages/web/src/common/constants/env.constants.ts b/packages/web/src/common/constants/env.constants.ts index 61e7091bf..33f9bb64f 100644 --- a/packages/web/src/common/constants/env.constants.ts +++ b/packages/web/src/common/constants/env.constants.ts @@ -41,10 +41,3 @@ export const ENV_WEB = webEnvSchema.parse({ }); export const IS_DEV = isDev(ENV_WEB.NODE_ENV); - -export const isGoogleAuthConfigured = (clientId?: string): boolean => - Boolean(clientId && clientId !== "undefined"); - -export const IS_GOOGLE_AUTH_CONFIGURED = isGoogleAuthConfigured( - ENV_WEB.GOOGLE_CLIENT_ID, -); diff --git a/packages/web/src/common/storage/migrations/data/legacy-event-to-local-record.transform.ts b/packages/web/src/common/storage/migrations/data/legacy-event-to-local-record.transform.ts index eb53382a1..14a38064a 100644 --- a/packages/web/src/common/storage/migrations/data/legacy-event-to-local-record.transform.ts +++ b/packages/web/src/common/storage/migrations/data/legacy-event-to-local-record.transform.ts @@ -2,12 +2,12 @@ import { ObjectId } from "bson"; import { type ValidatedCompassEvent } from "@core/types/compass-event.contracts"; import { type CalendarId, EventIdSchema } from "@core/types/domain-primitives"; import { type EventRecurrence } from "@core/types/event.contracts"; +import { getBrowserTimeZone } from "@web/common/utils/datetime/web.date.util"; +import { createObjectIdString } from "@web/common/utils/id/object-id.util"; import { type LocalEventRecord, LocalEventRecordSchema, -} from "@web/common/storage/types/local-event.record"; -import { getBrowserTimeZone } from "@web/common/utils/datetime/web.date.util"; -import { createObjectIdString } from "@web/common/utils/id/object-id.util"; +} from "@web/events/types/local-event.record"; /** Web-side mirror of `__compassDemoEvent` on legacy local-store rows. */ const LEGACY_DEMO_EVENT_FIELD = "__compassDemoEvent"; diff --git a/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts b/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts index 9698e8f03..e11ed84be 100644 --- a/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts +++ b/packages/web/src/common/storage/migrations/external/demo-data-seed.test.ts @@ -4,7 +4,7 @@ import { EventSchema } from "@core/types/event.contracts"; import dayjs from "@core/util/date/dayjs"; import { createMockOfflineDataStore } from "@web/__tests__/utils/storage/mock-offline-data-store.util"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; import { demoDataSeedMigration } from "./demo-data-seed"; import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; diff --git a/packages/web/src/common/storage/migrations/external/demo-data-seed.ts b/packages/web/src/common/storage/migrations/external/demo-data-seed.ts index c2a25576a..a1cbbb3d7 100644 --- a/packages/web/src/common/storage/migrations/external/demo-data-seed.ts +++ b/packages/web/src/common/storage/migrations/external/demo-data-seed.ts @@ -6,9 +6,9 @@ import { } from "@core/types/event.contracts"; import dayjs from "@core/util/date/dayjs"; import { getLocalCalendarSentinelId } from "@web/calendars/local-calendar.sentinel"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; import { getBrowserTimeZone } from "@web/common/utils/datetime/web.date.util"; import { createObjectIdString } from "@web/common/utils/id/object-id.util"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; import { getModifierKeyLabel } from "@web/shortcuts/shortcut.util"; import { VIEW_SHORTCUTS } from "@web/shortcuts/shortcuts.constants"; import { type OfflineDataStore } from "../../offline-data/offline-data.store"; diff --git a/packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts b/packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts index 5cdf4e8c9..e853efe3f 100644 --- a/packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts +++ b/packages/web/src/common/storage/offline-data/indexeddb-offline-data.store.ts @@ -3,7 +3,7 @@ import { type EventId } from "@core/types/domain-primitives"; import { type EventListQuery } from "@core/types/event-command.contracts"; import { getLocalCalendarSentinelId } from "@web/calendars/local-calendar.sentinel"; import { transformLegacyEvents } from "@web/common/storage/migrations/data/legacy-event-to-local-record.transform"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; import { deleteCompassLocalDb, extractDataFromLegacySchema, diff --git a/packages/web/src/common/storage/offline-data/offline-data.store.ts b/packages/web/src/common/storage/offline-data/offline-data.store.ts index 4a5cf94ba..a36c3fa33 100644 --- a/packages/web/src/common/storage/offline-data/offline-data.store.ts +++ b/packages/web/src/common/storage/offline-data/offline-data.store.ts @@ -1,6 +1,6 @@ import { type EventId } from "@core/types/domain-primitives"; import { type EventListQuery } from "@core/types/event-command.contracts"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; /** * Record of a completed migration. diff --git a/packages/web/src/common/utils/app-init.util.test.ts b/packages/web/src/common/utils/app-init.util.test.ts index 062d3e20b..c4042f10a 100644 --- a/packages/web/src/common/utils/app-init.util.test.ts +++ b/packages/web/src/common/utils/app-init.util.test.ts @@ -1,7 +1,7 @@ import { createTestToastPort } from "@web/__tests__/helpers/web-test-seams"; -import { DatabaseInitError } from "@web/common/utils/storage/db-errors.util"; import { registerToastPort } from "@web/common/utils/toast/toast.port"; import { + DatabaseInitError, initializeDatabaseWithErrorHandling, showDbInitErrorToast, } from "./app-init.util"; diff --git a/packages/web/src/common/utils/app-init.util.ts b/packages/web/src/common/utils/app-init.util.ts index d682d3228..b2e307182 100644 --- a/packages/web/src/common/utils/app-init.util.ts +++ b/packages/web/src/common/utils/app-init.util.ts @@ -1,21 +1,21 @@ import { initializeOfflineDataStore } from "@web/common/storage/offline-data/offline-data.store.registry"; import { getToast } from "@web/common/utils/toast/toast.port"; -import { DatabaseInitError } from "./storage/db-errors.util"; + +export class DatabaseInitError extends Error { + constructor( + message: string, + public readonly cause?: unknown, + ) { + super(message); + this.name = "DatabaseInitError"; + } +} export interface AppInitResult { dbInitError: DatabaseInitError | null; } -/** - * Initialize storage for the application. - * - * This: - * 1. Initializes the offline data store (IndexedDB with Dexie schema migrations) - * 2. Runs data migrations (storage-agnostic transformations) - * 3. Runs external migrations (imports from localStorage, etc.) - * - * Returns any initialization error so the caller can handle it appropriately. - */ +/** Initialize offline storage; return any init error so the app can keep running. */ export async function initializeDatabaseWithErrorHandling( initialize: typeof initializeOfflineDataStore = initializeOfflineDataStore, ): Promise { @@ -27,19 +27,14 @@ export async function initializeDatabaseWithErrorHandling( if (error instanceof DatabaseInitError) { dbInitError = error; } - // Continue app initialization - the app can still work without local storage - // by falling back to remote-only mode when authenticated + // Continue without local storage — authenticated users can use remote-only mode } return { dbInitError }; } -/** - * Show a toast notification for database initialization errors. - * Should be called after the app renders so the toast container is available. - */ +/** Defer toast until after render so the toast container is mounted. */ export function showDbInitErrorToast(dbInitError: DatabaseInitError): void { - // Use setTimeout to ensure toast container is mounted setTimeout(() => { getToast().error( `Compass can't use offline storage right now: ${dbInitError.message}. Your changes won't be saved on this device.`, diff --git a/packages/web/src/common/utils/datetime/web.date.util.test.ts b/packages/web/src/common/utils/datetime/web.date.util.test.ts index 0a30755c0..7f7328146 100644 --- a/packages/web/src/common/utils/datetime/web.date.util.test.ts +++ b/packages/web/src/common/utils/datetime/web.date.util.test.ts @@ -1,7 +1,6 @@ import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants"; import { type CompassEvent } from "@core/types/compass-event.contracts"; import dayjs from "@core/util/date/dayjs"; -import { arraysAreEqual } from "@web/__tests__/utils/web.test.util"; import { computeCurrentEventDateRange, computeRelativeEventDateRange, @@ -371,8 +370,7 @@ describe("getColorsByHour", () => { const day1Colors = getColorsByHour(day1.hour()); const day2Colors = getColorsByHour(day2.hour()); - const sameOrder = arraysAreEqual(day1Colors, day2Colors); - expect(sameOrder).toBe(true); + expect(day1Colors).toEqual(day2Colors); }); it("changes at the top of the hour", () => { @@ -381,8 +379,7 @@ describe("getColorsByHour", () => { const day1Colors = getColorsByHour(day1); const day2Colors = getColorsByHour(day2); - const sameOrder = arraysAreEqual(day1Colors, day2Colors); - expect(sameOrder).toBe(false); + expect(day1Colors).not.toEqual(day2Colors); }); }); diff --git a/packages/web/src/common/utils/resolve-default-export.util.ts b/packages/web/src/common/utils/resolve-default-export.util.ts deleted file mode 100644 index 73662bda9..000000000 --- a/packages/web/src/common/utils/resolve-default-export.util.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function resolveDefaultExport(moduleValue: T): T { - // Bun's __toESM(mod, nodeInterop=1) can wrap CJS+__esModule modules so - // .default points at the whole export object. Unwrap one level to reach the - // actual component function. - const maybeWrapped = moduleValue as T & { default?: T }; - - return maybeWrapped.default ?? moduleValue; -} diff --git a/packages/web/src/common/utils/storage/db-errors.util.ts b/packages/web/src/common/utils/storage/db-errors.util.ts deleted file mode 100644 index 3c17f3e7c..000000000 --- a/packages/web/src/common/utils/storage/db-errors.util.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Base error class for database-related errors - */ -class DatabaseError extends Error { - constructor( - message: string, - public readonly cause?: unknown, - ) { - super(message); - this.name = "DatabaseError"; - } -} - -/** - * Error thrown when database initialization fails - */ -export class DatabaseInitError extends DatabaseError { - constructor(message: string, cause?: unknown) { - super(message, cause); - this.name = "DatabaseInitError"; - } -} diff --git a/packages/web/src/common/utils/sync/local-event-sync.util.ts b/packages/web/src/common/utils/sync/local-event-sync.util.ts index a49873555..4840df959 100644 --- a/packages/web/src/common/utils/sync/local-event-sync.util.ts +++ b/packages/web/src/common/utils/sync/local-event-sync.util.ts @@ -1,13 +1,13 @@ import { type CreateEventInput } from "@core/types/event-command.contracts"; -import { CalendarApi } from "@web/calendars/calendar.api"; +import { CalendarApi } from "@web/api/calendar.api"; import { getLocalCalendar } from "@web/calendars/calendar.util"; import { type OfflineDataStore } from "@web/common/storage/offline-data/offline-data.store"; import { ensureOfflineDataStoreReady, getOfflineDataStore, } from "@web/common/storage/offline-data/offline-data.store.registry"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; import { EventApi } from "@web/events/event.api"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; type LocalEventSyncStorage = Pick< OfflineDataStore, diff --git a/packages/web/src/common/utils/toast/google-reconnect.toast.test.tsx b/packages/web/src/common/utils/toast/google-reconnect.toast.test.tsx index 6abd70216..aa74712f5 100644 --- a/packages/web/src/common/utils/toast/google-reconnect.toast.test.tsx +++ b/packages/web/src/common/utils/toast/google-reconnect.toast.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { createTestToastPort } from "@web/__tests__/helpers/web-test-seams"; -import { registerUseStartGoogleAuthorizationForTests } from "@web/auth/google/authorization/useStartGoogleAuthorization.registry"; +import { registerUseStartGoogleAuthorizationForTests } from "@web/auth/google/authorization/useStartGoogleAuthorization"; import { GoogleReconnectToast, showGoogleReconnectToast, diff --git a/packages/web/src/components/AuthModal/AuthModal.test.tsx b/packages/web/src/components/AuthModal/AuthModal.test.tsx index 130d7f990..7b430e323 100644 --- a/packages/web/src/components/AuthModal/AuthModal.test.tsx +++ b/packages/web/src/components/AuthModal/AuthModal.test.tsx @@ -18,7 +18,7 @@ import { resetEmailPasswordPort, } from "@web/auth/compass/hooks/emailpassword.port"; import { registerUseCompleteAuthenticationForTests } from "@web/auth/compass/hooks/useCompleteAuthentication.registry"; -import { registerUseStartGoogleAuthorizationForTests } from "@web/auth/google/authorization/useStartGoogleAuthorization.registry"; +import { registerUseStartGoogleAuthorizationForTests } from "@web/auth/google/authorization/useStartGoogleAuthorization"; import { resetGoogleAvailabilityForTests, setGoogleAvailabilityForTests, diff --git a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts index bcadc5a91..9d24ea3ee 100644 --- a/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts +++ b/packages/web/src/components/AuthModal/hooks/useAuthFormHandlers.ts @@ -8,7 +8,7 @@ import { type ResetPasswordFormData, type SignUpFormData, } from "@web/auth/compass/schemas/auth.schemas"; -import { releaseNotesPromptActions } from "@web/auth/state/release-notes-prompt.store"; +import { releaseNotesPromptActions } from "@web/components/ReleaseNotesPrompt/release-notes-prompt.store"; import { getAuthSubmitErrorMessage } from "./useAuthFormHandlers.util"; import { type AuthView } from "./useAuthModal"; diff --git a/packages/web/src/components/CommandPalette/CommandPalette.tsx b/packages/web/src/components/CommandPalette/CommandPalette.tsx index 799ee8d94..439ab8e6d 100644 --- a/packages/web/src/components/CommandPalette/CommandPalette.tsx +++ b/packages/web/src/components/CommandPalette/CommandPalette.tsx @@ -11,13 +11,8 @@ import { ArrowCounterClockwiseIcon } from "@phosphor-icons/react"; import { useNavigate } from "@tanstack/react-router"; import { useCallback, useRef, useState } from "react"; import { type SyncStatusVariant } from "@web/calendars/sync-status.types"; -import { eventCommandPaletteItems } from "@web/common/constants/event.cmd.constants"; -import { getMoreCommandPaletteSections } from "@web/common/constants/more.cmd.constants"; -import { - getNavigationCommandItems, - getNavigationViewRoute, -} from "@web/common/constants/navigation.cmd.constants"; import { Z_INDEX_MODAL } from "@web/common/constants/web.constants"; +import { eventCommandPaletteItems } from "@web/components/CommandPalette/event.cmd.constants"; import { useAuthCmdItems } from "@web/components/CommandPalette/hooks/useAuthCmdItems"; import { useCalendarSyncCmdItems } from "@web/components/CommandPalette/hooks/useCalendarSyncCmdItems"; import { useDeleteAccountCmdItems } from "@web/components/CommandPalette/hooks/useDeleteAccountCmdItems"; @@ -26,6 +21,11 @@ import { useExportDataCmdItems } from "@web/components/CommandPalette/hooks/useE import { useLogoutCmdItems } from "@web/components/CommandPalette/hooks/useLogoutCmdItems"; import { useSubscribeCmdItems } from "@web/components/CommandPalette/hooks/useSubscribeCmdItems"; import { useThemeCmdItems } from "@web/components/CommandPalette/hooks/useThemeCmdItems"; +import { getMoreCommandPaletteSections } from "@web/components/CommandPalette/more.cmd.constants"; +import { + getNavigationCommandItems, + getNavigationViewRoute, +} from "@web/components/CommandPalette/navigation.cmd.constants"; import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys"; import { type EventMutationDependencies } from "@web/events/mutations/useEventMutations"; import { useUndoRedo } from "@web/events/mutations/useUndoRedo"; diff --git a/packages/web/src/common/constants/event.cmd.constants.ts b/packages/web/src/components/CommandPalette/event.cmd.constants.ts similarity index 100% rename from packages/web/src/common/constants/event.cmd.constants.ts rename to packages/web/src/components/CommandPalette/event.cmd.constants.ts diff --git a/packages/web/src/common/constants/more.cmd.constants.test.ts b/packages/web/src/components/CommandPalette/more.cmd.constants.test.ts similarity index 96% rename from packages/web/src/common/constants/more.cmd.constants.test.ts rename to packages/web/src/components/CommandPalette/more.cmd.constants.test.ts index dc8db0f43..d2cd64c7a 100644 --- a/packages/web/src/common/constants/more.cmd.constants.test.ts +++ b/packages/web/src/components/CommandPalette/more.cmd.constants.test.ts @@ -1,7 +1,7 @@ import { getCommandPalettePlaceholder, getMoreCommandPaletteSections, -} from "@web/common/constants/more.cmd.constants"; +} from "@web/components/CommandPalette/more.cmd.constants"; import { feedbackActions, selectFeedbackRequest, diff --git a/packages/web/src/common/constants/more.cmd.constants.ts b/packages/web/src/components/CommandPalette/more.cmd.constants.ts similarity index 100% rename from packages/web/src/common/constants/more.cmd.constants.ts rename to packages/web/src/components/CommandPalette/more.cmd.constants.ts diff --git a/packages/web/src/common/constants/navigation.cmd.constants.test.ts b/packages/web/src/components/CommandPalette/navigation.cmd.constants.test.ts similarity index 97% rename from packages/web/src/common/constants/navigation.cmd.constants.test.ts rename to packages/web/src/components/CommandPalette/navigation.cmd.constants.test.ts index cf5946678..05f06cb16 100644 --- a/packages/web/src/common/constants/navigation.cmd.constants.test.ts +++ b/packages/web/src/components/CommandPalette/navigation.cmd.constants.test.ts @@ -1,7 +1,7 @@ import { type CommandPaletteViewName, getNavigationCommandItems, -} from "@web/common/constants/navigation.cmd.constants"; +} from "@web/components/CommandPalette/navigation.cmd.constants"; import { describe, expect, it } from "bun:test"; describe("getNavigationCommandItems", () => { diff --git a/packages/web/src/common/constants/navigation.cmd.constants.ts b/packages/web/src/components/CommandPalette/navigation.cmd.constants.ts similarity index 100% rename from packages/web/src/common/constants/navigation.cmd.constants.ts rename to packages/web/src/components/CommandPalette/navigation.cmd.constants.ts diff --git a/packages/web/src/components/DatePicker/DatePicker.tsx b/packages/web/src/components/DatePicker/DatePicker.tsx index 6fe9a0ed7..7cb09e8fb 100644 --- a/packages/web/src/components/DatePicker/DatePicker.tsx +++ b/packages/web/src/components/DatePicker/DatePicker.tsx @@ -7,7 +7,6 @@ import { darken, isDark } from "@web/common/styles/color.utils"; import { colors, lightColors } from "@web/common/styles/colors"; import { type CSSVariables } from "@web/common/styles/css.types"; import { theme } from "@web/common/styles/theme"; -import { resolveDefaultExport } from "@web/common/utils/resolve-default-export.util"; import { MonthNavButton } from "@web/components/DatePicker/MonthNavButton"; import { ChevronLeftIcon } from "@web/components/Icons/ChevronLeftIcon"; import { ChevronRightIcon } from "@web/components/Icons/ChevronRightIcon"; @@ -33,9 +32,14 @@ export interface Props extends Omit { type ReactDatePickerComponent = typeof ReactDatePickerModule.default; -const ReactDatePicker = resolveDefaultExport( - ReactDatePickerModule.default, -); +// Bun's __toESM(mod, nodeInterop=1) can wrap CJS+__esModule modules so +// .default points at the whole export object. Unwrap one level to reach the +// actual component function. +const reactDatePickerExport = + ReactDatePickerModule.default as ReactDatePickerComponent & { + default?: ReactDatePickerComponent; + }; +const ReactDatePicker = reactDatePickerExport.default ?? reactDatePickerExport; export const DatePicker: React.FC = (datePickerProps) => { const { diff --git a/packages/web/src/components/ReleaseNotesPrompt/ReleaseNotesPrompt.tsx b/packages/web/src/components/ReleaseNotesPrompt/ReleaseNotesPrompt.tsx index 15c092ebd..e6ca3796e 100644 --- a/packages/web/src/components/ReleaseNotesPrompt/ReleaseNotesPrompt.tsx +++ b/packages/web/src/components/ReleaseNotesPrompt/ReleaseNotesPrompt.tsx @@ -5,9 +5,9 @@ import { useState, } from "react"; import { UserApi } from "@web/api/user.api"; -import { releaseNotesPromptActions } from "@web/auth/state/release-notes-prompt.store"; import { Z_INDEX_MODAL } from "@web/common/constants/web.constants"; import { showErrorToast } from "@web/common/utils/toast/error-toast.util"; +import { releaseNotesPromptActions } from "@web/components/ReleaseNotesPrompt/release-notes-prompt.store"; import { PixelPirate } from "@web/components/WelcomeModal/PixelPirate"; type PromptState = diff --git a/packages/web/src/components/ReleaseNotesPrompt/release-notes-prompt.store.ts b/packages/web/src/components/ReleaseNotesPrompt/release-notes-prompt.store.ts new file mode 100644 index 000000000..5b6d9d532 --- /dev/null +++ b/packages/web/src/components/ReleaseNotesPrompt/release-notes-prompt.store.ts @@ -0,0 +1,93 @@ +import { create } from "zustand"; +import { persistentBrowserStore } from "@web/common/storage/browser-key-value.store"; + +export interface ReleaseNotesPromptState { + isOpen: boolean; +} + +export const useReleaseNotesPromptStore = create()( + () => ({ isOpen: false }), +); + +const SCHEDULED_OPEN_AT_KEY = + "compass.onboarding.release-notes-prompt-scheduled-at"; + +let timeoutId: number | undefined; + +const clearTimer = () => { + if (timeoutId !== undefined) { + window.clearTimeout(timeoutId); + timeoutId = undefined; + } +}; + +const clearSchedule = () => { + clearTimer(); + persistentBrowserStore.remove(SCHEDULED_OPEN_AT_KEY); +}; + +const setPromptOpen = (isOpen: boolean) => { + clearSchedule(); + useReleaseNotesPromptStore.setState({ isOpen }); +}; + +const startTimer = (delayMs: number) => { + clearTimer(); + timeoutId = window.setTimeout(() => { + timeoutId = undefined; + setPromptOpen(true); + }, delayMs); +}; + +const resumeScheduledOpen = () => { + if (!persistentBrowserStore.isAvailable()) return; + + const storedOpenAt = persistentBrowserStore.get(SCHEDULED_OPEN_AT_KEY); + if (storedOpenAt === null) return; + + const openAtMs = Number(storedOpenAt); + if (!Number.isFinite(openAtMs)) { + persistentBrowserStore.remove(SCHEDULED_OPEN_AT_KEY); + return; + } + + const remainingMs = openAtMs - Date.now(); + if (remainingMs <= 0) { + setPromptOpen(true); + return; + } + + startTimer(remainingMs); +}; + +export const releaseNotesPromptActions = { + open: () => setPromptOpen(true), + close: () => setPromptOpen(false), + scheduleOpen: (delayMs = 45_000) => { + if (persistentBrowserStore.isAvailable()) { + persistentBrowserStore.set( + SCHEDULED_OPEN_AT_KEY, + String(Date.now() + delayMs), + ); + } + startTimer(delayMs); + }, +}; + +export const selectReleaseNotesPromptOpen = (state: ReleaseNotesPromptState) => + state.isOpen; + +// E2e bridge: raise the post-signup prompt without a real signup. Merge so +// sibling store bridges survive. +if (typeof window !== "undefined") { + resumeScheduledOpen(); + + window.__COMPASS_E2E_STORE__ = { + ...window.__COMPASS_E2E_STORE__, + releaseNotesPrompt: { + getState: useReleaseNotesPromptStore.getState, + open: releaseNotesPromptActions.open, + close: releaseNotesPromptActions.close, + }, + }; +} diff --git a/packages/web/src/components/RootShell/RootShell.tsx b/packages/web/src/components/RootShell/RootShell.tsx index febb747b9..6a12ed0f6 100644 --- a/packages/web/src/components/RootShell/RootShell.tsx +++ b/packages/web/src/components/RootShell/RootShell.tsx @@ -1,11 +1,11 @@ import { Outlet } from "@tanstack/react-router"; -import { - selectReleaseNotesPromptOpen, - useReleaseNotesPromptStore, -} from "@web/auth/state/release-notes-prompt.store"; import { AuthModal } from "@web/components/AuthModal/AuthModal"; import { AuthModalProvider } from "@web/components/AuthModal/AuthModalProvider"; import { ReleaseNotesPrompt } from "@web/components/ReleaseNotesPrompt/ReleaseNotesPrompt"; +import { + selectReleaseNotesPromptOpen, + useReleaseNotesPromptStore, +} from "@web/components/ReleaseNotesPrompt/release-notes-prompt.store"; import { WelcomeGuideModal } from "@web/components/WelcomeModal/WelcomeGuideModal"; import { WelcomeModal } from "@web/components/WelcomeModal/WelcomeModal"; import { diff --git a/packages/web/src/components/SpaceCharacter/SpaceCharacter.tsx b/packages/web/src/components/SpaceCharacter/SpaceCharacter.tsx deleted file mode 100644 index 6ccdb7430..000000000 --- a/packages/web/src/components/SpaceCharacter/SpaceCharacter.tsx +++ /dev/null @@ -1 +0,0 @@ -export const SpaceCharacter = () => <> ; diff --git a/packages/web/src/components/SpaceCharacter/index.ts b/packages/web/src/components/SpaceCharacter/index.ts deleted file mode 100644 index 3e8a2b3ce..000000000 --- a/packages/web/src/components/SpaceCharacter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SpaceCharacter } from "./SpaceCharacter"; - -export { SpaceCharacter }; diff --git a/packages/web/src/events/queries/event.query.normalize.ts b/packages/web/src/events/queries/event.query.normalize.ts index a4a6896d1..abbdaeadd 100644 --- a/packages/web/src/events/queries/event.query.normalize.ts +++ b/packages/web/src/events/queries/event.query.normalize.ts @@ -1,6 +1,6 @@ import { type Event } from "@core/types/event.contracts"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; import { type NormalizedEvents } from "@web/events/event-view.types"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; /** * Normalize a list of events into the `{ ids, entities }` shape the query diff --git a/packages/web/src/events/repositories/local.event.repository.ts b/packages/web/src/events/repositories/local.event.repository.ts index a6b27eefd..bde262ecc 100644 --- a/packages/web/src/events/repositories/local.event.repository.ts +++ b/packages/web/src/events/repositories/local.event.repository.ts @@ -11,8 +11,8 @@ import { getOfflineDataStore, type OfflineDataStore, } from "@web/common/storage/offline-data/offline-data.store.registry"; -import { type LocalEventRecord } from "@web/common/storage/types/local-event.record"; import { createObjectIdString } from "@web/common/utils/id/object-id.util"; +import { type LocalEventRecord } from "@web/events/types/local-event.record"; import { type EventRepository } from "./event.repository.types"; /** diff --git a/packages/web/src/common/storage/types/local-event.record.test.ts b/packages/web/src/events/types/local-event.record.test.ts similarity index 95% rename from packages/web/src/common/storage/types/local-event.record.test.ts rename to packages/web/src/events/types/local-event.record.test.ts index b044609b5..2b1a288b1 100644 --- a/packages/web/src/common/storage/types/local-event.record.test.ts +++ b/packages/web/src/events/types/local-event.record.test.ts @@ -1,6 +1,6 @@ import { faker } from "@faker-js/faker"; import { EventSchema } from "@core/types/event.contracts"; -import { LocalEventRecordSchema } from "@web/common/storage/types/local-event.record"; +import { LocalEventRecordSchema } from "@web/events/types/local-event.record"; const validEvent = () => { const id = faker.database.mongodbObjectId(); diff --git a/packages/web/src/common/storage/types/local-event.record.ts b/packages/web/src/events/types/local-event.record.ts similarity index 100% rename from packages/web/src/common/storage/types/local-event.record.ts rename to packages/web/src/events/types/local-event.record.ts diff --git a/packages/web/src/grid/components/AllDayEventCard.tsx b/packages/web/src/grid/components/AllDayEventCard.tsx index 7d63571de..26ddd3e67 100644 --- a/packages/web/src/grid/components/AllDayEventCard.tsx +++ b/packages/web/src/grid/components/AllDayEventCard.tsx @@ -17,7 +17,6 @@ import { brighten, darken, isDark } from "@web/common/styles/color.utils"; import { theme } from "@web/common/styles/theme"; import { useEventPalette } from "@web/common/styles/theme.util"; import { type GridEvent } from "@web/common/types/web.event.types"; -import { SpaceCharacter } from "@web/components/SpaceCharacter"; import { EVENT_RESIZE_HANDLE_ATTRIBUTE } from "@web/grid/interaction/dom"; import { type EventPosition } from "@web/grid/types/grid.types"; import { EventRepeatIcon } from "./EventRepeatIcon"; @@ -168,7 +167,7 @@ const AllDayEventCardBase = ( style={{ color: titleColor }} > {event.title} - + {"\u00A0"} {showRepeatIcon && } diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index b5d5f7990..09cd41b6b 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -1,10 +1,10 @@ import { memo, useCallback, useMemo, useRef } from "react"; import dayjs from "@core/util/date/dayjs"; -import { getCommandPalettePlaceholder } from "@web/common/constants/more.cmd.constants"; import { ID_MAIN } from "@web/common/constants/web.constants"; import { useHorizontalNavigation } from "@web/common/hooks/useHorizontalNavigation"; import { emitViewCommand } from "@web/common/utils/dom/view-command-bus"; import { CommandPalette } from "@web/components/CommandPalette/CommandPalette"; +import { getCommandPalettePlaceholder } from "@web/components/CommandPalette/more.cmd.constants"; import { DemoEventsBannerGate } from "@web/components/DemoEventsBanner/DemoEventsBannerGate"; import { SidebarEventDetails } from "@web/components/Sidebar/EventDetails/SidebarEventDetails"; import { ResizableSidebarPanel } from "@web/components/Sidebar/ResizableSidebarPanel"; diff --git a/packages/web/src/views/GoogleAuthCallback/GoogleAuthCallback.tsx b/packages/web/src/views/GoogleAuthCallback/GoogleAuthCallback.tsx index 962909800..420eb58d5 100644 --- a/packages/web/src/views/GoogleAuthCallback/GoogleAuthCallback.tsx +++ b/packages/web/src/views/GoogleAuthCallback/GoogleAuthCallback.tsx @@ -6,9 +6,9 @@ import { useCompleteAuthentication } from "@web/auth/compass/hooks/useCompleteAu import { session } from "@web/auth/compass/session/Session"; import { refreshUserMetadata } from "@web/auth/compass/user/util/user-metadata.util"; import { completeGoogleAuthorization } from "@web/auth/google/authorization/complete-google-authorization"; -import { releaseNotesPromptActions } from "@web/auth/state/release-notes-prompt.store"; import { showErrorToast } from "@web/common/utils/toast/error-toast.util"; import { OverlayPanel } from "@web/components/OverlayPanel/OverlayPanel"; +import { releaseNotesPromptActions } from "@web/components/ReleaseNotesPrompt/release-notes-prompt.store"; import { eventQueryKeys } from "@web/events/queries/event.query.keys"; type CompleteAuthentication = ReturnType; diff --git a/packages/web/src/views/Life/LifeView.tsx b/packages/web/src/views/Life/LifeView.tsx index 24b31331d..a46819f98 100644 --- a/packages/web/src/views/Life/LifeView.tsx +++ b/packages/web/src/views/Life/LifeView.tsx @@ -1,11 +1,11 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { getCommandPalettePlaceholder } from "@web/common/constants/more.cmd.constants"; import { ROOT_ROUTES } from "@web/common/constants/routes"; import { ID_MAIN } from "@web/common/constants/web.constants"; import { useResponsiveLayout } from "@web/components/AuthenticatedLayout/useResponsiveLayout"; import { CalendarHeader } from "@web/components/CalendarHeader/CalendarHeader"; import { LifeCommandPalette } from "@web/components/CommandPalette/CommandPalette"; +import { getCommandPalettePlaceholder } from "@web/components/CommandPalette/more.cmd.constants"; import { ResizableSidebarPanel } from "@web/components/Sidebar/ResizableSidebarPanel"; import { SidebarShell } from "@web/components/Sidebar/SidebarShell"; import { useSidebarShortcuts } from "@web/components/Sidebar/useSidebarShortcuts"; diff --git a/packages/web/src/views/Week/WeekView.tsx b/packages/web/src/views/Week/WeekView.tsx index 83b35a72b..3474506e2 100644 --- a/packages/web/src/views/Week/WeekView.tsx +++ b/packages/web/src/views/Week/WeekView.tsx @@ -1,9 +1,9 @@ import { useCallback, useMemo, useRef } from "react"; -import { getCommandPalettePlaceholder } from "@web/common/constants/more.cmd.constants"; import { ID_MAIN } from "@web/common/constants/web.constants"; import { useHorizontalNavigation } from "@web/common/hooks/useHorizontalNavigation"; import { isEventFormOpen } from "@web/common/utils/form/form.util"; import { CommandPalette } from "@web/components/CommandPalette/CommandPalette"; +import { getCommandPalettePlaceholder } from "@web/components/CommandPalette/more.cmd.constants"; import { ContextMenuWrapper } from "@web/components/ContextMenu/GridContextMenuWrapper"; import { DemoEventsBannerGate } from "@web/components/DemoEventsBanner/DemoEventsBannerGate"; import { SidebarEventDetails } from "@web/components/Sidebar/EventDetails/SidebarEventDetails";