Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"classnames": "^2.3.1",
"css-loader": "^6.3.0",
"dayjs": "^1.10.7",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"fast-deep-equal": "^3.1.3",
"html-webpack-plugin": "^5.6.4",
"mini-css-extract-plugin": "^2.3.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/web/src/common/utils/auth/auth.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { session } from "@web/common/classes/Session";

/**
* Check if the user is currently authenticated
*/
export const isUserAuthenticated = async (): Promise<boolean> => {
try {
return await session.doesSessionExist();
} catch (error) {
console.error("Error checking authentication status:", error);
return false;
}
};

/**
* Get the user ID. Returns a placeholder ID if the user is not authenticated.
*/
export const getUserId = async (): Promise<string> => {
try {
const authenticated = await isUserAuthenticated();
if (!authenticated) {
return "local_user";
}

const accessTokenPayload =
(await session.getAccessTokenPayloadSecurely()) as {
sub: string;
};
return accessTokenPayload.sub;
} catch (error) {
console.error("Error getting user ID:", error);
return "local_user";
}
};
38 changes: 38 additions & 0 deletions packages/web/src/common/utils/storage/compass-local.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Dexie, { Table } from "dexie";
import { Schema_GridEvent } from "@web/common/types/web.event.types";

/**
* Extended event type for IndexedDB storage.
* Reuses the existing Schema_GridEvent type and ensures _id is present.
*/
export type ClientEvent = Schema_GridEvent & {
/**
* For local-only events, `_id` is a client-generated string.
* For now, we only persist local events here, not provider-backed ones.
*/
_id: string;
};

/**
* Dexie database for local storage of events and tasks.
* Used when user is not authenticated with Google Calendar.
*/
export class CompassLocalDB extends Dexie {
events!: Table<ClientEvent, string>;

constructor() {
super("compass-local");

// Version 1: initial schema
this.version(1).stores({
// Primary key and indexes
// _id is the PK; we also index on startDate/endDate/isSomeday for range queries
events: "_id, startDate, endDate, isSomeday, createdAt",
});

// Future versions: add .version(2)... etc with explicit migrations.
}
}

// Singleton instance of the database
export const compassLocalDB = new CompassLocalDB();
87 changes: 87 additions & 0 deletions packages/web/src/common/utils/storage/indexeddb.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Params_Events, Schema_Event } from "@core/types/event.types";
import { Schema_GridEvent } from "@web/common/types/web.event.types";
import { ClientEvent, compassLocalDB } from "./compass-local.db";

/**
* Save an event to IndexedDB
*/
export const saveEventToIndexedDB = async (
event: Schema_GridEvent,
): Promise<void> => {
if (!event._id) {
throw new Error("Event must have an _id to be saved to IndexedDB");
}

await compassLocalDB.events.put(event as ClientEvent);
};

/**
* Get events from IndexedDB based on query parameters
*/
export const getEventsFromIndexedDB = async (
params: Params_Events,
): Promise<Schema_Event[]> => {
const { startDate, endDate, someday } = params;

let query = compassLocalDB.events.toCollection();

// Filter by isSomeday
if (someday !== undefined) {
query = compassLocalDB.events
.where("isSomeday")
.equals(someday ? true : false);
}

// Get all matching events
let events = await query.toArray();

// Filter by date range if provided
if (startDate || endDate) {
events = events.filter((event) => {
if (!event.startDate) return false;

const eventStart = new Date(event.startDate).getTime();
const rangeStart = startDate ? new Date(startDate).getTime() : 0;
const rangeEnd = endDate
? new Date(endDate).getTime()
: Number.MAX_SAFE_INTEGER;

return eventStart >= rangeStart && eventStart <= rangeEnd;
});
}

return events as Schema_Event[];
};

/**
* Delete an event from IndexedDB
*/
export const deleteEventFromIndexedDB = async (_id: string): Promise<void> => {
await compassLocalDB.events.delete(_id);
};

/**
* Update an event in IndexedDB
*/
export const updateEventInIndexedDB = async (
_id: string,
updates: Partial<Schema_Event>,
): Promise<void> => {
const existing = await compassLocalDB.events.get(_id);
if (!existing) {
throw new Error(`Event with id "${_id}" not found in IndexedDB for update`);
}

await compassLocalDB.events.put({
...existing,
...updates,
_id, // Ensure _id is preserved
} as ClientEvent);
};

/**
* Clear all events from IndexedDB
*/
export const clearEventsFromIndexedDB = async (): Promise<void> => {
await compassLocalDB.events.clear();
};
89 changes: 76 additions & 13 deletions packages/web/src/ducks/events/sagas/event.sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ import {
Schema_OptimisticEvent,
Schema_WebEvent,
} from "@web/common/types/web.event.types";
import { isUserAuthenticated } from "@web/common/utils/auth/auth.util";
import { handleError } from "@web/common/utils/event/event.util";
import {
deleteEventFromIndexedDB,
getEventsFromIndexedDB,
saveEventToIndexedDB,
updateEventInIndexedDB,
} from "@web/common/utils/storage/indexeddb.util";
import { EventApi } from "@web/ducks/events/event.api";
import {
Action_ConvertEvent,
Expand Down Expand Up @@ -64,7 +71,18 @@ export function* convertCalendarToSomedayEvent({
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));

yield* _editEvent(gridEvent, { applyTo });
// Check if user is authenticated
const authenticated = (yield call(isUserAuthenticated)) as boolean;

if (authenticated) {
yield* _editEvent(gridEvent, { applyTo });
} else {
// Update event in IndexedDB for unauthenticated users
yield call(updateEventInIndexedDB, optimisticEvent._id, {
...gridEvent,
isSomeday: true,
});
}

yield put(
eventsEntitiesSlice.actions.edit({
Expand Down Expand Up @@ -99,7 +117,15 @@ export function* createEvent({ payload }: Action_CreateEvent) {
yield put(pendingEventsSlice.actions.add(event._id));

try {
yield call(EventApi.create, event);
// Check if user is authenticated
const authenticated = (yield call(isUserAuthenticated)) as boolean;

if (authenticated) {
yield call(EventApi.create, event);
} else {
// Save to IndexedDB for unauthenticated users
yield call(saveEventToIndexedDB, event);
}

yield put(
eventsEntitiesSlice.actions.edit({
Expand Down Expand Up @@ -132,7 +158,15 @@ export function* deleteEvent({ payload }: Action_DeleteEvent) {
const isPending = pendingEventIds.includes(payload._id);
// Only call delete API if event is not pending (i.e., exists in DB)
if (!isPending) {
yield call(EventApi.delete, payload._id, payload.applyTo);
// Check if user is authenticated
const authenticated = (yield call(isUserAuthenticated)) as boolean;

if (authenticated) {
yield call(EventApi.delete, payload._id, payload.applyTo);
} else {
// Delete from IndexedDB for unauthenticated users
yield call(deleteEventFromIndexedDB, payload._id);
}
}

yield put(deleteEventSlice.actions.success());
Expand All @@ -156,7 +190,15 @@ export function* editEvent({ payload }: Action_EditEvent) {
if (shouldRemove) yield put(eventsEntitiesSlice.actions.delete({ _id }));
else yield put(eventsEntitiesSlice.actions.edit(payload));

yield call(EventApi.edit, _id, event, { applyTo });
// Check if user is authenticated
const authenticated = (yield call(isUserAuthenticated)) as boolean;

if (authenticated) {
yield call(EventApi.edit, _id, event, { applyTo });
} else {
// Update in IndexedDB for unauthenticated users
yield call(updateEventInIndexedDB, _id, event);
}

// Remove from pending on success
yield put(pendingEventsSlice.actions.remove(_id));
Expand Down Expand Up @@ -198,16 +240,37 @@ function* getEvents(

const _payload = EventDateUtils.adjustStartEndDate(payload);

const res: Response_GetEventsSuccess = (yield call(
EventApi.get,
_payload,
)) as Response_GetEventsSuccess;
// Check if user is authenticated
const authenticated = (yield call(isUserAuthenticated)) as boolean;

const events = EventDateUtils.filterEventsByStartEndDate(
res.data,
payload.startDate as string,
payload.endDate as string,
);
let events: Schema_Event[] = [];

if (authenticated) {
// Fetch from API for authenticated users
const res: Response_GetEventsSuccess = (yield call(
EventApi.get,
_payload,
)) as Response_GetEventsSuccess;

events = EventDateUtils.filterEventsByStartEndDate(
res.data,
payload.startDate as string,
payload.endDate as string,
);
} else {
// Fetch from IndexedDB for unauthenticated users
const localEvents = (yield call(getEventsFromIndexedDB, {
someday: false,
startDate: _payload.startDate,
endDate: _payload.endDate,
})) as Schema_Event[];

events = EventDateUtils.filterEventsByStartEndDate(
localEvents,
payload.startDate as string,
payload.endDate as string,
);
}

const normalizedEvents = normalize<Schema_Event>(events, [
normalizedEventsSchema(),
Expand Down
Loading