From fc18709f0b8e0450101dd730e06b65422a03df40 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Mon, 9 Feb 2026 21:29:07 -0800 Subject: [PATCH 01/17] created notification schema --- models/Notification.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 models/Notification.ts diff --git a/models/Notification.ts b/models/Notification.ts new file mode 100644 index 0000000..659eabb --- /dev/null +++ b/models/Notification.ts @@ -0,0 +1,17 @@ +import mongoose from 'mongoose'; +const { Schema } = mongoose; + +const notificationSchema = new Schema({ +_id: { type: String, required: true }, +type: { type: String, required: true }, +labId: { type: String, required: true }, +resourceId: { type: String, required: true }, +recipients: { + type: [String], + required: true +}, +createdAt: { type: Date, required: true, default: Date.now } +}); +const Notification = mongoose.models.Notification || mongoose.model('Notification', notificationSchema); + +export default Notification; \ No newline at end of file From 5a7cc4fc9332f661f29e13c5582078ae1182d204 Mon Sep 17 00:00:00 2001 From: Alex Meng <122071020+AlexMeng0831@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:14:25 -0800 Subject: [PATCH 02/17] Create watchUpdates.ts --- services/notifications/watchUpdates.ts | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 services/notifications/watchUpdates.ts diff --git a/services/notifications/watchUpdates.ts b/services/notifications/watchUpdates.ts new file mode 100644 index 0000000..40b2ea8 --- /dev/null +++ b/services/notifications/watchUpdates.ts @@ -0,0 +1,34 @@ +import mongoose from "mongoose"; +import { connectToDatabase } from "@/lib/mongoose"; +import Notification from "@/models/Notification"; + +/** + * Starts a Change Stream watcher on the "products" collection. + * Inserts a DB_UPDATE notification whenever a product is updated. + */ +export async function startNotificationWatcher() { + await connectToDatabase(); + + const collection = mongoose.connection.collection("products"); + + const changeStream = collection.watch([], { + fullDocument: "updateLookup", + }); + + console.log("[notifications] watcher started"); + + for await (const change of changeStream) { + if (change.operationType !== "update") continue; + + const updatedDoc = change.fullDocument; + if (!updatedDoc) continue; + + await Notification.create({ + _id: `notif_${Date.now()}`, + type: "DB_UPDATE", + labId: updatedDoc.labId ?? "unknown", + resourceId: String(updatedDoc._id), + recipients: [], + }); + } +} From 893affcb40aa3569ab1e2887901e46516507a832 Mon Sep 17 00:00:00 2001 From: Alex Meng <122071020+AlexMeng0831@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:14:52 -0800 Subject: [PATCH 03/17] Create index.ts --- services/notifications/index.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 services/notifications/index.ts diff --git a/services/notifications/index.ts b/services/notifications/index.ts new file mode 100644 index 0000000..55ec001 --- /dev/null +++ b/services/notifications/index.ts @@ -0,0 +1,4 @@ +import { startNotificationWatcher } from "./watchUpdates"; + +// Entry point for notification service +startNotificationWatcher(); From 04f12902de567148a512d2b6983f53dcdcd77ab7 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Mon, 16 Feb 2026 21:02:17 -0800 Subject: [PATCH 04/17] fixed formatting --- models/Notification.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index 659eabb..656dd93 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -2,16 +2,18 @@ import mongoose from 'mongoose'; const { Schema } = mongoose; const notificationSchema = new Schema({ -_id: { type: String, required: true }, -type: { type: String, required: true }, -labId: { type: String, required: true }, -resourceId: { type: String, required: true }, -recipients: { - type: [String], - required: true -}, -createdAt: { type: Date, required: true, default: Date.now } + _id: { type: String, required: true }, + type: { type: String, required: true }, + labId: { type: String, required: true }, + resourceId: { type: String, required: true }, + recipients: { + type: [String], + required: true + }, + createdAt: { type: Date, required: true, default: Date.now } }); -const Notification = mongoose.models.Notification || mongoose.model('Notification', notificationSchema); + +const Notification = mongoose.models.Notification || + mongoose.model('Notification', notificationSchema); export default Notification; \ No newline at end of file From de8b1d264dd2d6217f06f711e97a5c84a7d6668b Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Mon, 16 Feb 2026 21:07:46 -0800 Subject: [PATCH 05/17] remove console.log, changed products to items, added useful comments --- services/notifications/watchUpdates.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/notifications/watchUpdates.ts b/services/notifications/watchUpdates.ts index 40b2ea8..bc94060 100644 --- a/services/notifications/watchUpdates.ts +++ b/services/notifications/watchUpdates.ts @@ -9,17 +9,20 @@ import Notification from "@/models/Notification"; export async function startNotificationWatcher() { await connectToDatabase(); - const collection = mongoose.connection.collection("products"); + const collection = mongoose.connection.collection("items"); const changeStream = collection.watch([], { fullDocument: "updateLookup", }); - console.log("[notifications] watcher started"); - for await (const change of changeStream) { if (change.operationType !== "update") continue; + /** + * Get the updated document from the change stream + * If quantity is below the threshold, continue + * else send a notification + */ const updatedDoc = change.fullDocument; if (!updatedDoc) continue; From 3f2be2001d2fbfe59a1581377c468d077e66504f Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Tue, 17 Feb 2026 09:34:09 -0800 Subject: [PATCH 06/17] deleted id field, move labId to top, add roles field to recipients --- models/Notification.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index 656dd93..c1b1aba 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -2,13 +2,13 @@ import mongoose from 'mongoose'; const { Schema } = mongoose; const notificationSchema = new Schema({ - _id: { type: String, required: true }, - type: { type: String, required: true }, labId: { type: String, required: true }, + type: { type: String, required: true }, resourceId: { type: String, required: true }, recipients: { - type: [String], - required: true + type: [String], + roles: { type: String }, + required: true }, createdAt: { type: Date, required: true, default: Date.now } }); From c65962176d154a95e1bec85f18ce2651daa8f3de Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Wed, 18 Feb 2026 17:33:41 -0800 Subject: [PATCH 07/17] updated comments --- services/notifications/watchUpdates.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/services/notifications/watchUpdates.ts b/services/notifications/watchUpdates.ts index bc94060..8c45217 100644 --- a/services/notifications/watchUpdates.ts +++ b/services/notifications/watchUpdates.ts @@ -18,14 +18,15 @@ export async function startNotificationWatcher() { for await (const change of changeStream) { if (change.operationType !== "update") continue; - /** - * Get the updated document from the change stream - * If quantity is below the threshold, continue - * else send a notification - */ + // Get the updated document from the change stream const updatedDoc = change.fullDocument; + + // If there’s no document, skip this iteration if (!updatedDoc) continue; + // TODO: Check the quantity in updatedDoc + // If quantity is below the threshold, continue + // Otherwise, send a notification await Notification.create({ _id: `notif_${Date.now()}`, type: "DB_UPDATE", From 8248fe00a3abf8ab926e8f8dcdcb5b8bf9329040 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Fri, 20 Feb 2026 10:49:24 -0800 Subject: [PATCH 08/17] change recipients to list with role instead of type String --- models/Notification.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index c1b1aba..a1b5b2a 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -5,11 +5,12 @@ const notificationSchema = new Schema({ labId: { type: String, required: true }, type: { type: String, required: true }, resourceId: { type: String, required: true }, - recipients: { - type: [String], - roles: { type: String }, - required: true - }, + recipients: [ + { + role: { type: String }, + required: true + } + ], createdAt: { type: Date, required: true, default: Date.now } }); From 896989350e6d9a3765d8172e63eef8f51412983e Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Sun, 5 Apr 2026 18:21:00 -0700 Subject: [PATCH 09/17] fix recipients structure --- models/Notification.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/Notification.ts b/models/Notification.ts index a1b5b2a..d4316c1 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -7,7 +7,8 @@ const notificationSchema = new Schema({ resourceId: { type: String, required: true }, recipients: [ { - role: { type: String }, + type: [String], + enum: ["PI", "LAB_MANAGER", "RESEARCHER"], required: true } ], From 5fdd7fd92c4c46ad7a56a6cafe2b5382c8861830 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Sun, 5 Apr 2026 20:32:09 -0700 Subject: [PATCH 10/17] fix recipients structure --- models/Notification.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index d4316c1..cb95182 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -7,9 +7,12 @@ const notificationSchema = new Schema({ resourceId: { type: String, required: true }, recipients: [ { + role: { + type: [String], + enum: ["PI", "LAB_MANAGER", "RESEARCHER"], + required: true + }, type: [String], - enum: ["PI", "LAB_MANAGER", "RESEARCHER"], - required: true } ], createdAt: { type: Date, required: true, default: Date.now } From 963a41edadcf3a24ed5f7d2dd5be6a57b7c1bf22 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Wed, 15 Apr 2026 19:33:55 -0700 Subject: [PATCH 11/17] added unit tests --- models/__tests__/Notification.test.ts | 31 ++++++++++++++++++ services/__tests__/Notification.test.ts | 42 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 models/__tests__/Notification.test.ts create mode 100644 services/__tests__/Notification.test.ts diff --git a/models/__tests__/Notification.test.ts b/models/__tests__/Notification.test.ts new file mode 100644 index 0000000..a596ed3 --- /dev/null +++ b/models/__tests__/Notification.test.ts @@ -0,0 +1,31 @@ +import Notification from "../Notification"; + +describe("Notification Schema", () => { + test("valid user passes", async () => { + const notification = new Notification({ + labId: "lab123", + type: "DB_UPDATE", + resourceId: "item123", + recipients: [ + { + role: ["PI", "RESEARCHER"], + type: ["EMAIL"] + } + ] + }); + await expect(notification.validate()).resolves.toBeUndefined(); + }); + + test("missing required field fails", async () => { + const notification = new Notification({ + type: "DB_UPDATE", + recipients: [ + { + role: ["PI", "RESEARCHER"], + type: ["EMAIL"] + } + ] + }); + await expect(notification.validate()).rejects.toThrow(); + }); +}) \ No newline at end of file diff --git a/services/__tests__/Notification.test.ts b/services/__tests__/Notification.test.ts new file mode 100644 index 0000000..fb0d9c8 --- /dev/null +++ b/services/__tests__/Notification.test.ts @@ -0,0 +1,42 @@ +jest.doMock("@/lib/mongoose", () => ({ + connectToDatabase: jest.fn(), +})); + +jest.doMock("@/models/Notification", () => ({ + create: jest.fn(), +})); + +import mongoose from "mongoose"; + +describe("startNotificationWatcher", () => { + let startNotificationWatcher: any; + let Notification: any; + let connectToDatabase: jest.Mock; + + beforeAll(async () => { + // IMPORTANT: import AFTER mocks + ({ startNotificationWatcher } = await import("../notifications/watchUpdates")); + ({ default: Notification } = await import("@/models/Notification")); + }); + + test("creates notification", async () => { + const mockCollection = { + watch: jest.fn(), + }; + + (mongoose.connection.collection as jest.Mock).mockReturnValue(mockCollection); + + mockCollection.watch.mockReturnValue({ + async *[Symbol.asyncIterator]() { + yield { + operationType: "update", + fullDocument: { _id: "item1", labId: "lab1" }, + }; + }, + }); + + Notification.create.mockResolvedValue({}); + + await startNotificationWatcher(); + }); +}); \ No newline at end of file From c308385d25eebe7f9bc82986f440d0f0ccd7b50c Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Mon, 27 Apr 2026 20:48:17 -0700 Subject: [PATCH 12/17] co-purchase popups front end work components --- components/Copurchase.module.css | 124 +++++++++++++++++++++++++++++++ components/CopurchaseInvite.tsx | 55 ++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 components/Copurchase.module.css create mode 100644 components/CopurchaseInvite.tsx diff --git a/components/Copurchase.module.css b/components/Copurchase.module.css new file mode 100644 index 0000000..8eea451 --- /dev/null +++ b/components/Copurchase.module.css @@ -0,0 +1,124 @@ +.modal { + background: #ffffff; + width: 456px; + border-radius: 10px; + border: 1px solid #d8d8d8; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); + padding: 25px; + position: relative; + font-family: Georgia, serif; +} + +.closeBtn { + position: absolute; + top: 17px; + right: 17px; + width: 16px; + height: 16px; + border-radius: 2px; + border: none; + background: transparent; + cursor: pointer; + opacity: 0.7; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: opacity 0.15s; +} + +.closeBtn:hover { + opacity: 1; +} + +.header { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; + padding-right: 24px; +} + +.title { + font-size: 17px; + font-weight: 700; + color: #1a1a1a; + line-height: 1.2; +} + +.subtitle { + font-size: 13px; + color: #666; + line-height: 1.4; +} + +.input { + width: 100%; + height: 36px; + border-radius: 8px; + border: 1px solid #d8d8d8; + padding: 4px 12px; + font-family: Georgia, serif; + font-size: 13px; + color: #444; + background: #fafafa; + outline: none; + margin-bottom: 14px; + box-sizing: border-box; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.input:focus { + border-color: #2e6b8a; + box-shadow: 0 0 0 3px rgba(46, 107, 138, 0.12); +} + +.actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.btnPrimary { + height: 36px; + border-radius: 8px; + border: none; + cursor: pointer; + font-family: Georgia, serif; + font-size: 13px; + font-weight: 600; + padding: 0 20px; + background: #2e6b8a; + color: #fff; + transition: opacity 0.15s, transform 0.1s; +} + +.btnPrimary:hover { + opacity: 0.88; +} + +.btnPrimary:active { + transform: scale(0.97); +} + +.btnSecondary { + height: 36px; + border-radius: 8px; + border: none; + cursor: pointer; + font-family: Georgia, serif; + font-size: 13px; + font-weight: 600; + padding: 0 20px; + background: #f0f0f0; + color: #444; + transition: opacity 0.15s, transform 0.1s; +} + +.btnSecondary:hover { + opacity: 0.75; +} + +.btnSecondary:active { + transform: scale(0.97); +} \ No newline at end of file diff --git a/components/CopurchaseInvite.tsx b/components/CopurchaseInvite.tsx new file mode 100644 index 0000000..c4f2996 --- /dev/null +++ b/components/CopurchaseInvite.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import styles from "./CoPurchase.module.css"; + +const CloseIcon = () => ( + + + + +); + +interface InviteModalProps { + onClose: () => void; + onSend: (email: string) => void; +} + +export default function CopurchaseInvite({ onClose, onSend }: InviteModalProps) { + const [email, setEmail] = useState(""); + + const handleSend = () => { + onSend(email); + onClose(); + }; + + return ( +
+ + +
+
Invite to Co-purchase
+
+ Invite other labs or colleagues to split the cost of this item. +
+
+ + setEmail(e.target.value)} + /> + +
+ + +
+
+ ); +} \ No newline at end of file From 3ca5911f80751f7b3f59e488253fa7e8f8ab1c0c Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Fri, 1 May 2026 17:39:51 -0700 Subject: [PATCH 13/17] fixing unit tests --- models/Notification.ts | 12 +++-- models/__tests__/Notification.test.ts | 15 +++--- services/__tests__/Notification.test.ts | 66 +++++++++++++++---------- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index cb95182..ea7daf5 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -7,12 +7,14 @@ const notificationSchema = new Schema({ resourceId: { type: String, required: true }, recipients: [ { - role: { - type: [String], + role: [{ + type: String, + required: true, enum: ["PI", "LAB_MANAGER", "RESEARCHER"], - required: true - }, - type: [String], + }], + type: [{ + type: String, + }], } ], createdAt: { type: Date, required: true, default: Date.now } diff --git a/models/__tests__/Notification.test.ts b/models/__tests__/Notification.test.ts index a596ed3..b0fb3e0 100644 --- a/models/__tests__/Notification.test.ts +++ b/models/__tests__/Notification.test.ts @@ -1,5 +1,8 @@ +import { describe } from "node:test"; +import test, { expect } from "playwright/test"; import Notification from "../Notification"; + describe("Notification Schema", () => { test("valid user passes", async () => { const notification = new Notification({ @@ -9,9 +12,9 @@ describe("Notification Schema", () => { recipients: [ { role: ["PI", "RESEARCHER"], - type: ["EMAIL"] - } - ] + type: ["EMAIL"], + }, + ], }); await expect(notification.validate()).resolves.toBeUndefined(); }); @@ -22,9 +25,9 @@ describe("Notification Schema", () => { recipients: [ { role: ["PI", "RESEARCHER"], - type: ["EMAIL"] - } - ] + type: ["EMAIL"], + }, + ], }); await expect(notification.validate()).rejects.toThrow(); }); diff --git a/services/__tests__/Notification.test.ts b/services/__tests__/Notification.test.ts index fb0d9c8..92f6e72 100644 --- a/services/__tests__/Notification.test.ts +++ b/services/__tests__/Notification.test.ts @@ -1,42 +1,56 @@ -jest.doMock("@/lib/mongoose", () => ({ - connectToDatabase: jest.fn(), -})); - -jest.doMock("@/models/Notification", () => ({ - create: jest.fn(), -})); - import mongoose from "mongoose"; +const mockCollection = { + watch: jest.fn(), +}; + +beforeEach(() => { + jest.spyOn(mongoose.connection, "collection").mockReturnValue(mockCollection as any); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +jest.mock("mongoose", () => { + const mockWatch = jest.fn(); + const mockCollection = { watch: mockWatch }; + return { + ...jest.requireActual("mongoose"), + connection: { + collection: jest.fn(() => mockCollection), + }, + }; +}); + describe("startNotificationWatcher", () => { let startNotificationWatcher: any; let Notification: any; - let connectToDatabase: jest.Mock; beforeAll(async () => { - // IMPORTANT: import AFTER mocks ({ startNotificationWatcher } = await import("../notifications/watchUpdates")); - ({ default: Notification } = await import("@/models/Notification")); + ({ default: Notification } = await import("../../models/Notification")); }); - test("creates notification", async () => { - const mockCollection = { - watch: jest.fn(), +test("creates notification on update event", async () => { +const fakeStream = { + async *[Symbol.asyncIterator]() { + yield { + operationType: "update", + fullDocument: { + _id: "item1", + labId: "lab1", + }, }; + }, +}; - (mongoose.connection.collection as jest.Mock).mockReturnValue(mockCollection); - - mockCollection.watch.mockReturnValue({ - async *[Symbol.asyncIterator]() { - yield { - operationType: "update", - fullDocument: { _id: "item1", labId: "lab1" }, - }; - }, - }); - - Notification.create.mockResolvedValue({}); + jest.spyOn(mongoose.connection, "collection").mockReturnValue({ + watch: jest.fn(() => fakeStream), + } as any); await startNotificationWatcher(); + + expect(Notification.create).toHaveBeenCalled(); }); }); \ No newline at end of file From 1ddf6f1c826de7244a7f2a2822d6f3d104c00bda Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Fri, 1 May 2026 17:47:33 -0700 Subject: [PATCH 14/17] fixing unit tests --- models/__tests__/Notification.test.ts | 2 -- services/notifications/watchUpdates.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/models/__tests__/Notification.test.ts b/models/__tests__/Notification.test.ts index b0fb3e0..0d983f1 100644 --- a/models/__tests__/Notification.test.ts +++ b/models/__tests__/Notification.test.ts @@ -1,5 +1,3 @@ -import { describe } from "node:test"; -import test, { expect } from "playwright/test"; import Notification from "../Notification"; diff --git a/services/notifications/watchUpdates.ts b/services/notifications/watchUpdates.ts index 8c45217..a1fa716 100644 --- a/services/notifications/watchUpdates.ts +++ b/services/notifications/watchUpdates.ts @@ -28,7 +28,6 @@ export async function startNotificationWatcher() { // If quantity is below the threshold, continue // Otherwise, send a notification await Notification.create({ - _id: `notif_${Date.now()}`, type: "DB_UPDATE", labId: updatedDoc.labId ?? "unknown", resourceId: String(updatedDoc._id), From 3fe41560f55de251d84504f56817f9275f306cc8 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Fri, 1 May 2026 17:57:02 -0700 Subject: [PATCH 15/17] fixing unit tests --- models/Notification.ts | 2 +- models/__tests__/Notification.test.ts | 4 +- services/__tests__/Notification.test.ts | 69 ++++++++++++------------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/models/Notification.ts b/models/Notification.ts index ea7daf5..98f0992 100644 --- a/models/Notification.ts +++ b/models/Notification.ts @@ -12,7 +12,7 @@ const notificationSchema = new Schema({ required: true, enum: ["PI", "LAB_MANAGER", "RESEARCHER"], }], - type: [{ + channel: [{ type: String, }], } diff --git a/models/__tests__/Notification.test.ts b/models/__tests__/Notification.test.ts index 0d983f1..2addc92 100644 --- a/models/__tests__/Notification.test.ts +++ b/models/__tests__/Notification.test.ts @@ -10,7 +10,7 @@ describe("Notification Schema", () => { recipients: [ { role: ["PI", "RESEARCHER"], - type: ["EMAIL"], + channel: ["EMAIL"], }, ], }); @@ -23,7 +23,7 @@ describe("Notification Schema", () => { recipients: [ { role: ["PI", "RESEARCHER"], - type: ["EMAIL"], + channel: ["EMAIL"], }, ], }); diff --git a/services/__tests__/Notification.test.ts b/services/__tests__/Notification.test.ts index 92f6e72..365c88c 100644 --- a/services/__tests__/Notification.test.ts +++ b/services/__tests__/Notification.test.ts @@ -1,53 +1,52 @@ import mongoose from "mongoose"; +import { startNotificationWatcher } from "../notifications/watchUpdates"; +import Notification from "@/models/Notification"; + +jest.mock("mongoose", () => ({ + ...jest.requireActual("mongoose"), + connection: { + collection: jest.fn(), + }, +})); + +jest.mock("@/models/Notification", () => ({ + __esModule: true, + default: { + create: jest.fn(), + }, +})); + +jest.mock("../../lib/mongoose", () => ({ + connectToDatabase: jest.fn(), +})); const mockCollection = { watch: jest.fn(), }; beforeEach(() => { - jest.spyOn(mongoose.connection, "collection").mockReturnValue(mockCollection as any); + (mongoose.connection.collection as jest.Mock).mockReturnValue(mockCollection); }); afterEach(() => { - jest.restoreAllMocks(); -}); - -jest.mock("mongoose", () => { - const mockWatch = jest.fn(); - const mockCollection = { watch: mockWatch }; - return { - ...jest.requireActual("mongoose"), - connection: { - collection: jest.fn(() => mockCollection), - }, - }; + jest.clearAllMocks(); }); describe("startNotificationWatcher", () => { - let startNotificationWatcher: any; - let Notification: any; - - beforeAll(async () => { - ({ startNotificationWatcher } = await import("../notifications/watchUpdates")); - ({ default: Notification } = await import("../../models/Notification")); - }); - -test("creates notification on update event", async () => { -const fakeStream = { - async *[Symbol.asyncIterator]() { - yield { - operationType: "update", - fullDocument: { - _id: "item1", - labId: "lab1", - }, + test("creates notification on update event", async () => { + const fakeStream = { + async *[Symbol.asyncIterator]() { + yield { + operationType: "update", + fullDocument: { + _id: "item1", + labId: "lab1", + }, + }; + }, }; - }, -}; - jest.spyOn(mongoose.connection, "collection").mockReturnValue({ - watch: jest.fn(() => fakeStream), - } as any); + mockCollection.watch.mockReturnValue(fakeStream); await startNotificationWatcher(); From 57655a7abf18304bfbba94adab854b33f4a7e803 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Tue, 5 May 2026 08:18:25 -0700 Subject: [PATCH 16/17] front end copurchase popups --- .../{ => copurchase}/Copurchase.module.css | 4 -- .../{ => copurchase}/CopurchaseInvite.tsx | 9 +--- components/copurchase/CopurchaseLabInput.tsx | 45 +++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) rename components/{ => copurchase}/Copurchase.module.css (93%) rename components/{ => copurchase}/CopurchaseInvite.tsx (74%) create mode 100644 components/copurchase/CopurchaseLabInput.tsx diff --git a/components/Copurchase.module.css b/components/copurchase/Copurchase.module.css similarity index 93% rename from components/Copurchase.module.css rename to components/copurchase/Copurchase.module.css index 8eea451..28a41af 100644 --- a/components/Copurchase.module.css +++ b/components/copurchase/Copurchase.module.css @@ -6,7 +6,6 @@ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); padding: 25px; position: relative; - font-family: Georgia, serif; } .closeBtn { @@ -58,7 +57,6 @@ border-radius: 8px; border: 1px solid #d8d8d8; padding: 4px 12px; - font-family: Georgia, serif; font-size: 13px; color: #444; background: #fafafa; @@ -84,7 +82,6 @@ border-radius: 8px; border: none; cursor: pointer; - font-family: Georgia, serif; font-size: 13px; font-weight: 600; padding: 0 20px; @@ -106,7 +103,6 @@ border-radius: 8px; border: none; cursor: pointer; - font-family: Georgia, serif; font-size: 13px; font-weight: 600; padding: 0 20px; diff --git a/components/CopurchaseInvite.tsx b/components/copurchase/CopurchaseInvite.tsx similarity index 74% rename from components/CopurchaseInvite.tsx rename to components/copurchase/CopurchaseInvite.tsx index c4f2996..c996e70 100644 --- a/components/CopurchaseInvite.tsx +++ b/components/copurchase/CopurchaseInvite.tsx @@ -1,13 +1,6 @@ import { useState } from "react"; import styles from "./CoPurchase.module.css"; -const CloseIcon = () => ( - - - - -); - interface InviteModalProps { onClose: () => void; onSend: (email: string) => void; @@ -24,7 +17,7 @@ export default function CopurchaseInvite({ onClose, onSend }: InviteModalProps) return (
diff --git a/components/copurchase/CopurchaseLabInput.tsx b/components/copurchase/CopurchaseLabInput.tsx new file mode 100644 index 0000000..6762b8b --- /dev/null +++ b/components/copurchase/CopurchaseLabInput.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import styles from "./CoPurchase.module.css"; + +interface InviteModalProps { + onClose: () => void; + onSend: (email: string) => void; +} + +export default function CopurchaseInvite({ onClose, onSend }: InviteModalProps) { + const [email, setEmail] = useState(""); + + const handleSend = () => { + onSend(email); + onClose(); + }; + + return ( +
+ + +
+
Enter your lab
+
+ Enter the name of your desired lab +
+
+ + setEmail(e.target.value)} + /> + +
+ +
+
+ ); +} \ No newline at end of file From 26e8c552a3653d7f85f6214b2b13013c61637411 Mon Sep 17 00:00:00 2001 From: emmanishikawa Date: Tue, 5 May 2026 08:20:14 -0700 Subject: [PATCH 17/17] front-end copurchase popup --- components/copurchase/CopurchaseLabInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/copurchase/CopurchaseLabInput.tsx b/components/copurchase/CopurchaseLabInput.tsx index 6762b8b..a98b39a 100644 --- a/components/copurchase/CopurchaseLabInput.tsx +++ b/components/copurchase/CopurchaseLabInput.tsx @@ -31,7 +31,7 @@ export default function CopurchaseInvite({ onClose, onSend }: InviteModalProps) className={styles.input} type="email" value={email} - placeholder="colleague@ucsd.edu" + placeholder="Dr. Xu" onChange={(e) => setEmail(e.target.value)} />