From f4051eb43098c13ff14fa5465a470cf1d462ef7d Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 17 Aug 2025 21:41:21 -0400 Subject: [PATCH 01/17] Go back an indent in code --- src/components/layout/Navbar.tsx | 140 +++++++++++++++---------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index f3c095f..76f87eb 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -48,80 +48,80 @@ const Navbar:FC = () => { ECE Pathmaker logo
-
+ href="/courses" + > + Courses + + +
  • + setIsMenuOpen(false)} + className=" + hover:text-green2 + transition-all + mx-auto + " + href="/faq" + > + FAQs + +
  • +
  • + +
  • + + + ) From 6e6e80acee495c6f79f8676f512eba3bd43de993 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 17 Aug 2025 23:37:08 -0400 Subject: [PATCH 02/17] Enlarge theme toggle button --- src/components/ThemeToggle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index dac3942..7c4f40a 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -16,7 +16,7 @@ const ThemeToggle = () => { return ( From 57bab8e7224f8bc299ce089a864f201ca0f75bce Mon Sep 17 00:00:00 2001 From: Sunny Date: Sun, 17 Aug 2025 23:38:03 -0400 Subject: [PATCH 03/17] Implement basic Firebase Google sign in --- package-lock.json | 883 ++++++++++++++++++++++++++++++- package.json | 1 + src/components/SignIn.tsx | 32 ++ src/components/layout/Navbar.tsx | 4 + src/contexts/AuthContext.tsx | 45 ++ src/firebase/auth.ts | 22 + src/firebase/firebase.ts | 24 + src/main.tsx | 9 +- src/types/types.tsx | 9 +- 9 files changed, 1015 insertions(+), 14 deletions(-) create mode 100644 src/components/SignIn.tsx create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/firebase/auth.ts create mode 100644 src/firebase/firebase.ts diff --git a/package-lock.json b/package-lock.json index 53d37ff..d572e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@vercel/analytics": "^1.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "firebase": "^12.1.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "lucide-react": "^0.474.0", @@ -1441,6 +1442,614 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@firebase/ai": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.1.0.tgz", + "integrity": "sha512-4HvFr4YIzNFh0MowJLahOjJDezYSTjQar0XYVu/sAycoxQ+kBsfXuTPRLVXCYfMR5oNwQgYe4Q2gAOYKKqsOyA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.18.tgz", + "integrity": "sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.24.tgz", + "integrity": "sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.18", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.1.tgz", + "integrity": "sha512-jxTrDbxnGoX7cGz7aP9E7v9iKvBbQfZ8Gz4TH3SfrrkcyIojJM3+hJnlbGnGxHrABts844AxRcg00arMZEyA6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.1.tgz", + "integrity": "sha512-BEy1L6Ufd85ZSP79HDIv0//T9p7d5Bepwy+2mKYkgdXBGKTbFm2e2KxyF1nq4zSQ6RRBxWi0IY0zFVmoBTZlUA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.1", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.0.tgz", + "integrity": "sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.4", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.0.tgz", + "integrity": "sha512-4O7v4VFeSEwAZtLjsaj33YrMHMRjplOIYC2CiYsF6o/MboOhrhe01VrTt8iY9Y5EwjRHuRz4pS6jMBT8LfQYJA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.0.tgz", + "integrity": "sha512-2/LH5xIbD8aaLOWSFHAwwAybgSzHIM0dB5oVOL0zZnxFG1LctX2bc1NIAaPk1T+Zo9aVkLKUlB5fTXTkVUQprQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.0.tgz", + "integrity": "sha512-VPgtvoGFywWbQqtvgJnVWIDFSHV1WE6Hmyi5EGI+P+56EskiGkmnw6lEqc/MEUfGpPGdvmc4I9XMU81uj766/g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.6.tgz", + "integrity": "sha512-Yelp5xd8hM4NO1G1SuWrIk4h5K42mNwC98eWZ9YLVu6Z0S6hFk1mxotAdCRmH2luH8FASlYgLLq6OQLZ4nbnCA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.19.tgz", + "integrity": "sha512-y7PZAb0l5+5oIgLJr88TNSelxuASGlXyAKj+3pUc4fDuRIdPNBoONMHaIUa9rlffBR5dErmaD2wUBJ7Z1a513Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz", + "integrity": "sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ==", + "license": "Apache-2.0" + }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -1475,6 +2084,37 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2478,6 +3118,70 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -3653,7 +4357,6 @@ "version": "22.10.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", - "devOptional": true, "dependencies": { "undici-types": "~6.20.0" } @@ -4631,7 +5334,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -5268,7 +5970,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5607,6 +6308,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -5698,6 +6411,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.1.0.tgz", + "integrity": "sha512-oZucxvfWKuAW4eHHRqGKzC43fLiPqPwHYBHPRNsnkgonqYaq0VurYgqgBosRlEulW+TWja/5Tpo2FpUU+QrfEQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.1.0", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/app": "0.14.1", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.1", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-compat": "0.4.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-compat": "0.4.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-compat": "0.2.19", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5820,7 +6593,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6000,6 +6772,12 @@ "node": ">=8.0.0" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -6052,6 +6830,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -7546,6 +8330,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7560,6 +8350,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8374,6 +9170,30 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -8669,7 +9489,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8822,6 +9641,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9556,7 +10395,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -9793,6 +10631,12 @@ "makeerror": "1.0.12" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -9803,6 +10647,29 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -9869,7 +10736,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -9965,7 +10831,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -9994,7 +10859,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -10013,7 +10877,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 188e706..8eddf75 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@vercel/analytics": "^1.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "firebase": "^12.1.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", "lucide-react": "^0.474.0", diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx new file mode 100644 index 0000000..d03a584 --- /dev/null +++ b/src/components/SignIn.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { CiLogin, CiLogout } from "react-icons/ci"; +import { signInGoogle, signOut } from "../firebase/auth"; +import { useAuth } from "../contexts/AuthContext"; + + +const SignIn:FC = () => { + const { signedIn } = useAuth(); + // const [isSigningIn, setIsSigningIn] = useState(false); + + const signIn = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + await signInGoogle(); + } catch { + console.log("Google sign in failed.") + } + } + + return ( +
    + +
    + ) +} + +export default SignIn; \ No newline at end of file diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 76f87eb..9601142 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -2,6 +2,7 @@ import { IoIosMenu, IoIosClose } from "react-icons/io"; import { FC, useState, useEffect, useRef } from 'react'; import { logo } from '../../utils/assetImports'; import ThemeToggle from "../ThemeToggle"; +import SignIn from "../SignIn"; const Navbar:FC = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -108,6 +109,9 @@ const Navbar:FC = () => { FAQs +
  • + +
  • diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..dab6902 --- /dev/null +++ b/src/contexts/AuthContext.tsx @@ -0,0 +1,45 @@ +import { AuthContextType } from "../types/types"; +import { auth } from "../firebase/firebase"; +import { onAuthStateChanged, User } from "firebase/auth"; +import { createContext, ReactNode, useContext, useEffect, useState } from "react"; + +const AuthContext = createContext({ + user: null, + signedIn: false, + loading: true, +}); + +/* eslint-disable react-refresh/only-export-components */ +export const useAuth = () => { + return useContext(AuthContext); +} + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const initializeUser = async (newUser: User | null) => { + if (newUser) { + setUser(newUser); + } else { + setUser(null); + } + + setLoading(false); + } + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, initializeUser); + return unsubscribe; + }, []); + + return ( + + {!loading && children} + + ) +} \ No newline at end of file diff --git a/src/firebase/auth.ts b/src/firebase/auth.ts new file mode 100644 index 0000000..e83b3d7 --- /dev/null +++ b/src/firebase/auth.ts @@ -0,0 +1,22 @@ +import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"; +import { auth } from "./firebase"; +import { signOut as firebaseSignOut } from "firebase/auth"; + +const signInGoogle = async () => { + const provider = new GoogleAuthProvider; + const result = await signInWithPopup(auth, provider); + // do something with result.user in firestore + console.log(result.user) + return result; +} + +const signOut = async () => { + try { + await firebaseSignOut(auth); + console.log("Signed out."); + } catch (err) { + console.error("Sign out failed: ", err); + } +}; + +export { signInGoogle, signOut }; \ No newline at end of file diff --git a/src/firebase/firebase.ts b/src/firebase/firebase.ts new file mode 100644 index 0000000..725d707 --- /dev/null +++ b/src/firebase/firebase.ts @@ -0,0 +1,24 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +import { getAnalytics } from "firebase/analytics"; +import { getAuth } from 'firebase/auth'; +import { getFirestore } from 'firebase/firestore'; + +const firebaseConfig = { + apiKey: "AIzaSyCqNwV308wggSrbOzpZKp2YXo18_vC-_Pg", + authDomain: "ecepathmaker.firebaseapp.com", + projectId: "ecepathmaker", + storageBucket: "ecepathmaker.firebasestorage.app", + messagingSenderId: "471899088952", + appId: "1:471899088952:web:69ee9480dc6f6f4bb19ecd", + measurementId: "G-LXD116CKJL" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +const analytics = getAnalytics(app); +const auth = getAuth(app); +const db = getFirestore(app); + +export { app, analytics, auth, db }; + diff --git a/src/main.tsx b/src/main.tsx index 4c5839e..4438c98 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,16 +2,19 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { AuthProvider } from './contexts/AuthContext.tsx'; const storedTheme = localStorage.getItem('theme'); if (storedTheme === 'dark') { - document.documentElement.classList.add('dark'); + document.documentElement.classList.add('dark'); } else if (storedTheme === 'light') { - document.documentElement.classList.remove('dark'); + document.documentElement.classList.remove('dark'); } createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/src/types/types.tsx b/src/types/types.tsx index 1523062..f46cff4 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -1,4 +1,5 @@ import { UniqueIdentifier } from "@dnd-kit/core"; +import { User } from "firebase/auth"; interface CourseIdentifier { code: string; @@ -189,4 +190,10 @@ export interface FilterState { isSciMath: boolean; isArtSci: boolean; isEng: boolean; -}; \ No newline at end of file +}; + +export interface AuthContextType { + user: User | null; + signedIn: boolean; + loading: boolean; +} \ No newline at end of file From 168c3fe9bda142c33cca2b8154d98880e9d57681 Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 6 Oct 2025 01:03:51 -0400 Subject: [PATCH 04/17] Implement firestore for saving layouts along with helper functions for other state variables --- Use context to pass savedLayout state variable and setter between Maker and SignIn components Ensure the savedLayouts array does not have undefined values when writing to firestore document --- src/components/SignIn.tsx | 53 +++++++++++++++----------- src/components/forms/LoadLayout.tsx | 2 +- src/components/forms/SaveLayout.tsx | 7 ++++ src/components/layout/Footer.tsx | 2 +- src/components/layout/Layout.tsx | 52 ++++++++++++++++++++----- src/firebase/firestore.ts | 59 +++++++++++++++++++++++++++++ src/pages/Maker.tsx | 38 +++++++++---------- 7 files changed, 158 insertions(+), 55 deletions(-) create mode 100644 src/firebase/firestore.ts diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx index d03a584..f7c4065 100644 --- a/src/components/SignIn.tsx +++ b/src/components/SignIn.tsx @@ -1,32 +1,39 @@ -import { FC } from 'react'; +import { FC } from "react"; import { CiLogin, CiLogout } from "react-icons/ci"; import { signInGoogle, signOut } from "../firebase/auth"; import { useAuth } from "../contexts/AuthContext"; +import { useLayoutContext } from "./layout/Layout"; +import { loadLayouts } from "../firebase/firestore"; +const SignIn: FC = () => { + const { signedIn } = useAuth(); -const SignIn:FC = () => { - const { signedIn } = useAuth(); - // const [isSigningIn, setIsSigningIn] = useState(false); + const { setSavedLayouts } = useLayoutContext(); - const signIn = async (e: React.MouseEvent) => { - e.preventDefault(); - try { - await signInGoogle(); - } catch { - console.log("Google sign in failed.") - } + const signIn = async (e: React.MouseEvent) => { + e.preventDefault(); + const result = await signInGoogle(); + const user = result.user; + try { + const remoteSavedLayouts = await loadLayouts(user.uid); + if (remoteSavedLayouts.length > 0) { + setSavedLayouts(remoteSavedLayouts); + } + } catch (err) { + console.error("Failed to load layouts:", err); } + }; - return ( -
    - -
    - ) -} + return ( +
    + +
    + ); +}; -export default SignIn; \ No newline at end of file +export default SignIn; diff --git a/src/components/forms/LoadLayout.tsx b/src/components/forms/LoadLayout.tsx index e77d9c8..e017898 100644 --- a/src/components/forms/LoadLayout.tsx +++ b/src/components/forms/LoadLayout.tsx @@ -96,7 +96,7 @@ const LoadLayout: FC = ({
    {savedLayouts.map( (layout, index) => - layout && ( + layout?.name && ( = ({ setSavedLayouts((prev) => { const newLayouts = [...prev]; + + // Fill any missing indices with default objects, important for firestore + for (let i = 0; i <= saveIndex; i++) { + if (!newLayouts[i]?.name) newLayouts[i] = { name: "", str: "" }; + } + newLayouts[saveIndex] = newLayout; + return newLayouts; }); diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index f8e005e..c08f8dd 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -10,7 +10,7 @@ const Footer:FC = () => { }; const resetLocalStorage = () => { - if (window.confirm(`Are you sure you want to reset everything saved in local storage? + if (window.confirm(`Are you sure you want to reset all saved data in local storage and database? This will remove your custom courses, undo your edits, and clear your layout. This will also allow updates to take effect.`)) { const savedTheme = localStorage.getItem('theme'); localStorage.clear(); diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 01bc953..23f8189 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,23 +1,55 @@ -import { FC, ReactNode } from "react"; +import { + FC, + ReactNode, + createContext, + useContext, + useState, +} from "react"; import { Analytics } from "@vercel/analytics/react"; import { Footer, Navbar } from "../../utils/componentImports"; +import { savedLayout } from "../../types/types"; interface LayoutProps { children: ReactNode; } +interface LayoutContextType { + savedLayouts: savedLayout[]; + setSavedLayouts: React.Dispatch>; +} + +// eslint-disable-next-line react-refresh/only-export-components +export const LayoutContext = createContext( + undefined +); + const Layout: FC = ({ children }) => { + const [savedLayouts, setSavedLayouts] = useState(() => { + const saved = localStorage.getItem("savedLayouts"); + return saved ? JSON.parse(saved) : []; + }); + return ( -
    -
    - -
    - {children} - -
    -
    -
    + +
    +
    + +
    + {children} + +
    +
    +
    + ); }; +// eslint-disable-next-line react-refresh/only-export-components +export function useLayoutContext() { + const ctx = useContext(LayoutContext); + if (!ctx) + throw new Error("useLayoutContext must be used within LayoutProvider"); + return ctx; +} + export default Layout; diff --git a/src/firebase/firestore.ts b/src/firebase/firestore.ts new file mode 100644 index 0000000..355d6c7 --- /dev/null +++ b/src/firebase/firestore.ts @@ -0,0 +1,59 @@ +import { doc, setDoc, getDoc } from "firebase/firestore"; +import { db } from "./firebase"; +import { CourseList, CoursesOnGrid, CoursesUsed, savedLayout } from "../types/types" +import { UniqueIdentifier } from "@dnd-kit/core"; + +// ========== HELPERS ========== + +// Save generic data for a user (merge keeps other fields intact) +async function saveUserData(uid: string, key: string, value: unknown) { + await setDoc( + doc(db, "users", uid), + { [key]: value }, + { merge: true } + ); +} + +async function loadUserData(uid: string, key: string, fallback: T): Promise { + const ref = doc(db, "users", uid); + const snap = await getDoc(ref); + if (snap.exists()) { + return (snap.data()[key] as T) ?? fallback; + } + return fallback; +} + +export async function saveCourses(uid: string, courses: CourseList) { + return saveUserData(uid, "courses", courses); +} +export async function loadCourses(uid: string) { + return loadUserData(uid, "courses", []); +} + +export async function saveCoursesUsed(uid: string, coursesUsed: CoursesUsed) { + return saveUserData(uid, "coursesUsed", coursesUsed); +} +export async function loadCoursesUsed(uid: string) { + return loadUserData(uid, "coursesUsed", {}); +} + +export async function saveCoursesOnGrid(uid: string, grid: CoursesOnGrid) { + return saveUserData(uid, "coursesOnGrid", grid); +} +export async function loadCoursesOnGrid(uid: string) { + return loadUserData(uid, "coursesOnGrid", []); +} + +export async function saveDependencies(uid: string, deps: Map>) { + return saveUserData(uid, "dependencies", deps); +} +export async function loadDependencies(uid: string) { + return loadUserData(uid, "dependencies", []); +} + +export async function saveLayouts(uid: string, layouts: savedLayout[]) { + return saveUserData(uid, "savedLayouts", layouts); +} +export async function loadLayouts(uid: string) { + return loadUserData(uid, "savedLayouts", []); +} diff --git a/src/pages/Maker.tsx b/src/pages/Maker.tsx index f3e6b3d..0e3aa2b 100644 --- a/src/pages/Maker.tsx +++ b/src/pages/Maker.tsx @@ -1,5 +1,5 @@ import { useMemo, useState, useEffect } from "react"; -import { CourseCardProps, CoursesOnGrid, CoursesUsed, savedLayout } from "../types/types"; +import { CourseCardProps, CoursesOnGrid, CoursesUsed } from "../types/types"; import { mockCourses } from "../utils/dataImports"; import { CourseForm, @@ -9,8 +9,13 @@ import { } from "../utils/componentImports"; import { UniqueIdentifier } from "@dnd-kit/core"; import { emptyGrid } from "../utils/utilImports"; +import { auth } from "../firebase/firebase"; +import { saveLayouts } from "../firebase/firestore"; +import { useLayoutContext } from "../components/layout/Layout"; const Maker = () => { + const { savedLayouts, setSavedLayouts } = useLayoutContext(); + // Course info for custom course const [customInfo, setCustomInfo] = useState({ code: "", @@ -67,10 +72,12 @@ const Maker = () => { }, [coursesOnGrid]); // Map of co/prerequisites to their dependencies - const initialDependencies = useMemo>>(() => { + const initialDependencies = useMemo< + Map> + >(() => { const saved = localStorage.getItem("dependencies"); if (!saved) { - return new Map(); + return new Map(); } try { @@ -83,21 +90,16 @@ const Maker = () => { const [dependencies, setDependencies] = useState>>(initialDependencies); useEffect(() => { - const obj: [UniqueIdentifier, UniqueIdentifier[]][] = Array.from(dependencies.entries()).map( - ([key, valueSet]) => [key, Array.from(valueSet)] - ); + const obj: [UniqueIdentifier, UniqueIdentifier[]][] = Array.from( + dependencies.entries() + ).map(([key, valueSet]) => [key, Array.from(valueSet)]); localStorage.setItem("dependencies", JSON.stringify(obj)); }, [dependencies]); - - // Saved layouts - const [savedLayouts, setSavedLayouts] = useState(() => { - const saved = localStorage.getItem("savedLayouts"); - if (saved) return JSON.parse(saved); - return []; - }); useEffect(() => { localStorage.setItem("savedLayouts", JSON.stringify(savedLayouts)); + const user = auth.currentUser; + if (user) saveLayouts(user.uid, savedLayouts); }, [savedLayouts]); return ( @@ -117,9 +119,7 @@ const Maker = () => {
    -

    - Load layout -

    +

    Load layout

    Paste your previously copied string or load from cache

    @@ -132,13 +132,11 @@ const Maker = () => { setDependencies={setDependencies} savedLayouts={savedLayouts} /> -

    - Save layout -

    +

    Save layout

    Copy this string and save it for later or store layout in cache

    - Date: Fri, 10 Oct 2025 00:43:43 -0400 Subject: [PATCH 05/17] Save grid and dependencies in firestore --- src/components/SignIn.tsx | 39 +++++++-- src/components/forms/SaveLayout.tsx | 2 +- src/components/grid/CourseGrid.tsx | 46 ++++++----- src/components/layout/Footer.tsx | 2 +- src/components/layout/Layout.tsx | 73 +++++++++++++++-- src/data/faqData.tsx | 4 + src/firebase/auth.ts | 2 - src/firebase/firestore.ts | 122 +++++++++++++++++++++++++--- src/pages/Maker.tsx | 75 ++++++----------- src/types/types.tsx | 19 +++++ 10 files changed, 282 insertions(+), 102 deletions(-) diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx index f7c4065..80bdabc 100644 --- a/src/components/SignIn.tsx +++ b/src/components/SignIn.tsx @@ -3,24 +3,49 @@ import { CiLogin, CiLogout } from "react-icons/ci"; import { signInGoogle, signOut } from "../firebase/auth"; import { useAuth } from "../contexts/AuthContext"; import { useLayoutContext } from "./layout/Layout"; -import { loadLayouts } from "../firebase/firestore"; +import { loadCourses, loadCoursesOnGrid, loadCoursesUsed, loadDependencies, loadLayouts } from "../firebase/firestore"; const SignIn: FC = () => { const { signedIn } = useAuth(); - const { setSavedLayouts } = useLayoutContext(); + const { + setSavedLayouts, + setCourses, + setCoursesUsed, + setCoursesOnGrid, + setDependencies + } = useLayoutContext(); const signIn = async (e: React.MouseEvent) => { e.preventDefault(); const result = await signInGoogle(); const user = result.user; + try { - const remoteSavedLayouts = await loadLayouts(user.uid); - if (remoteSavedLayouts.length > 0) { - setSavedLayouts(remoteSavedLayouts); - } + // Fetch everything in parallel + const [ + remoteSavedLayouts, + remoteCourses, + remoteCoursesUsed, + remoteCoursesOnGrid, + remoteDependencies, + ] = await Promise.all([ + loadLayouts(user.uid), + loadCourses(user.uid), + loadCoursesUsed(user.uid), + loadCoursesOnGrid(user.uid), + loadDependencies(user.uid), + ]); + + // Apply loaded data only if available + if (remoteSavedLayouts?.length) setSavedLayouts(remoteSavedLayouts); + if (Object.keys(remoteCourses ?? {}).length) setCourses(remoteCourses); + if (Object.keys(remoteCoursesUsed ?? {}).length) setCoursesUsed(remoteCoursesUsed); + if (Object.keys(remoteCoursesOnGrid ?? {}).length) setCoursesOnGrid(remoteCoursesOnGrid); + if (remoteDependencies?.size) setDependencies(remoteDependencies); + } catch (err) { - console.error("Failed to load layouts:", err); + console.error("Failed to load user data:", err); } }; diff --git a/src/components/forms/SaveLayout.tsx b/src/components/forms/SaveLayout.tsx index 83d6246..788aaa8 100644 --- a/src/components/forms/SaveLayout.tsx +++ b/src/components/forms/SaveLayout.tsx @@ -171,7 +171,7 @@ const SaveLayout: FC = ({ {save === Save.SUCCESS && ( - Layout saved in cache! + Layout saved! )} ); diff --git a/src/components/grid/CourseGrid.tsx b/src/components/grid/CourseGrid.tsx index fe50197..6ccdb63 100644 --- a/src/components/grid/CourseGrid.tsx +++ b/src/components/grid/CourseGrid.tsx @@ -6,6 +6,7 @@ import { StreamRequirements, GridPosition, CoursesUsed, + GridPositionBase, } from "../../types/types"; import { Droppable, @@ -500,27 +501,32 @@ const CourseGrid: FC = ({ ref={screenshotRef} data-testid="grid" > - {Object.entries(coursesOnGrid).map(([slot, courseCode]) => ( - - {courseCode ? ( - { + const courseCode: string = coursesOnGrid[slot as GridPositionBase]; + return ( + - ) : ( - slot - )} - - ))} + > + {courseCode ? ( + + ) : ( + slot + )} + + ) + })} + +
    {/* Courses to choose from */} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index c08f8dd..0787fe3 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -10,7 +10,7 @@ const Footer:FC = () => { }; const resetLocalStorage = () => { - if (window.confirm(`Are you sure you want to reset all saved data in local storage and database? + if (window.confirm(`Are you sure you want to reset all saved data in local storage and cloud (if signed in)? This will remove your custom courses, undo your edits, and clear your layout. This will also allow updates to take effect.`)) { const savedTheme = localStorage.getItem('theme'); localStorage.clear(); diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 23f8189..53033c3 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -3,34 +3,89 @@ import { ReactNode, createContext, useContext, + useMemo, useState, } from "react"; import { Analytics } from "@vercel/analytics/react"; import { Footer, Navbar } from "../../utils/componentImports"; -import { savedLayout } from "../../types/types"; +import { CoursesOnGrid, CoursesUsed, LayoutContextType, savedLayout } from "../../types/types"; +import { UniqueIdentifier } from "@dnd-kit/core"; +import { emptyGrid } from "../../utils/emptyGrid"; +import mockCourses from "../../data/mockCourses"; interface LayoutProps { children: ReactNode; } -interface LayoutContextType { - savedLayouts: savedLayout[]; - setSavedLayouts: React.Dispatch>; -} - // eslint-disable-next-line react-refresh/only-export-components export const LayoutContext = createContext( undefined ); const Layout: FC = ({ children }) => { + const [courses, setCourses] = useState(() => { + const savedCourses = localStorage.getItem("courses"); + if (savedCourses) return JSON.parse(savedCourses); + return mockCourses; + }); + + // coursesUsed + const initialCoursesUsed = useMemo(() => { + const savedCoursesUsed = localStorage.getItem("coursesUsed"); + if (savedCoursesUsed) return JSON.parse(savedCoursesUsed); + + const posMap: CoursesUsed = {}; + Object.keys(courses).forEach((courseCode) => (posMap[courseCode] = "")); + return posMap; + }, [courses]); + const [coursesUsed, setCoursesUsed] = + useState(initialCoursesUsed); + + // coursesOnGrid + const initialCoursesOnGrid = useMemo(() => { + const savedCoursesOnGrid = localStorage.getItem("coursesOnGrid"); + if (savedCoursesOnGrid) return JSON.parse(savedCoursesOnGrid); + return emptyGrid; + }, []); + const [coursesOnGrid, setCoursesOnGrid] = + useState(initialCoursesOnGrid); + + // Map of co/prerequisites to their dependencies + const initialDependencies = useMemo< + Map> + >(() => { + const saved = localStorage.getItem("dependencies"); + if (!saved) return new Map(); + + try { + const parsed: [string, string[]][] = JSON.parse(saved); + return new Map(parsed.map(([key, values]) => [key, new Set(values)])); + } catch { + return new Map(); + } + }, []); + const [dependencies, setDependencies] = + useState>>(initialDependencies); const [savedLayouts, setSavedLayouts] = useState(() => { const saved = localStorage.getItem("savedLayouts"); return saved ? JSON.parse(saved) : []; }); return ( - +
    @@ -45,11 +100,11 @@ const Layout: FC = ({ children }) => { }; // eslint-disable-next-line react-refresh/only-export-components -export function useLayoutContext() { +export function useLayoutContext(): LayoutContextType { const ctx = useContext(LayoutContext); if (!ctx) throw new Error("useLayoutContext must be used within LayoutProvider"); return ctx; } -export default Layout; +export default Layout; \ No newline at end of file diff --git a/src/data/faqData.tsx b/src/data/faqData.tsx index 415752d..b3a47fc 100644 --- a/src/data/faqData.tsx +++ b/src/data/faqData.tsx @@ -49,6 +49,10 @@ export const siteFaqData: FAQ = [ question: "What are the XX slots for?", answer: "The fifth row is for extra courses taken in the first two years or from overloading. A course there will be considered as an early prerequisite, and any course on XX slots have their prerequisites waived." }, + { + question: "What does signing in do?", + answer: "When you sign in using your Google account, your grid, saved layouts, custom courses, and light/dark theme preference are saved so you can access your changes on any device." + }, { question: "How do I update a course (e.g. change the colour)?", answer: "Click on a Maker course card, click on the pencil in the top right, then update its details in the form." diff --git a/src/firebase/auth.ts b/src/firebase/auth.ts index e83b3d7..a9cd561 100644 --- a/src/firebase/auth.ts +++ b/src/firebase/auth.ts @@ -5,8 +5,6 @@ import { signOut as firebaseSignOut } from "firebase/auth"; const signInGoogle = async () => { const provider = new GoogleAuthProvider; const result = await signInWithPopup(auth, provider); - // do something with result.user in firestore - console.log(result.user) return result; } diff --git a/src/firebase/firestore.ts b/src/firebase/firestore.ts index 355d6c7..145afd0 100644 --- a/src/firebase/firestore.ts +++ b/src/firebase/firestore.ts @@ -1,6 +1,6 @@ import { doc, setDoc, getDoc } from "firebase/firestore"; import { db } from "./firebase"; -import { CourseList, CoursesOnGrid, CoursesUsed, savedLayout } from "../types/types" +import { CourseCardPropsWithoutCode, CourseList, CoursesOnGrid, CoursesUsed, savedLayout } from "../types/types" import { UniqueIdentifier } from "@dnd-kit/core"; // ========== HELPERS ========== @@ -23,37 +23,135 @@ async function loadUserData(uid: string, key: string, fallback: T): Promise Array.isArray(r) ? JSON.stringify(r) : r); +} + +// Decode preq/coreq from Firestore and restore nested arrays +function decodeCourseRequirements(req?: string[]): (string | string[])[] { + if (!req) return []; + return req.map(r => { + try { + const parsed = JSON.parse(r); + if (Array.isArray(parsed)) return parsed; + } catch { + // Not a JSON string, leave as-is + } + return r; + }); +} + +// Serialize CourseList for Firestore +export function serializeCoursesForFirestore(courses: CourseList) { + const serialized: Record = {}; + for (const [code, course] of Object.entries(courses)) { + serialized[code] = { + ...course, + preq: encodeCourseRequirements(course.preq), + coreq: encodeCourseRequirements(course.coreq), + }; + } + return serialized; +} + +type SerializedCourseCard = Omit & { + preq?: string[]; + coreq?: string[]; +}; + +type SerializedCourseList = Record; + +export function deserializeCoursesFromFirestore(data: SerializedCourseList): CourseList { + const courses: CourseList = {}; + for (const [code, course] of Object.entries(data)) { + courses[code] = { + ...course, + preq: decodeCourseRequirements(course.preq), + coreq: decodeCourseRequirements(course.coreq), + }; + } + return courses; +} + +// for maps and sets since Firestore doesn't support saving them +function serializeForFirestore(data: unknown): unknown { + if (data instanceof Map) { + // Convert Map to a plain object + return Object.fromEntries( + Array.from(data.entries(), ([key, value]) => [key, serializeForFirestore(value)]) + ); + } else if (Array.isArray(data)) { + // Recursively handle nested arrays + return data.map(serializeForFirestore); + } else if (typeof data === "object" && data !== null) { + // Recursively handle nested objects + const result: Record = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = serializeForFirestore(value); + } + return result; + } + return data; +} + +function deserializeFromFirestore(data: unknown): unknown { + if (typeof data === "object" && data !== null) { + if (!Array.isArray(data)) { + // Optionally, turn certain objects back into Maps + if (Object.values(data).every(v => typeof v === "object")) { + return new Map( + Object.entries(data).map(([k, v]) => [k, deserializeFromFirestore(v)]) + ); + } + const result: Record = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = deserializeFromFirestore(value); + } + return result; + } else { + return data.map(deserializeFromFirestore); + } + } + return data; +} + export async function saveCourses(uid: string, courses: CourseList) { - return saveUserData(uid, "courses", courses); + return saveUserData(uid, "courses", serializeCoursesForFirestore(courses)); } -export async function loadCourses(uid: string) { - return loadUserData(uid, "courses", []); +export async function loadCourses(uid: string): Promise { + const raw = await loadUserData>(uid, "courses", {}); + return deserializeCoursesFromFirestore(raw); } export async function saveCoursesUsed(uid: string, coursesUsed: CoursesUsed) { return saveUserData(uid, "coursesUsed", coursesUsed); } export async function loadCoursesUsed(uid: string) { - return loadUserData(uid, "coursesUsed", {}); + return loadUserData(uid, "coursesUsed", {}); } export async function saveCoursesOnGrid(uid: string, grid: CoursesOnGrid) { return saveUserData(uid, "coursesOnGrid", grid); } export async function loadCoursesOnGrid(uid: string) { - return loadUserData(uid, "coursesOnGrid", []); + return loadUserData(uid, "coursesOnGrid", {} as CoursesOnGrid); } -export async function saveDependencies(uid: string, deps: Map>) { - return saveUserData(uid, "dependencies", deps); +export async function saveDependencies( + uid: string, + deps: Map> +) { + return saveUserData(uid, "dependencies", serializeForFirestore(deps)); } -export async function loadDependencies(uid: string) { - return loadUserData(uid, "dependencies", []); +export async function loadDependencies(uid: string): Promise>> { + return deserializeFromFirestore(loadUserData>>(uid, "dependencies", {} as Map>)) as Map>; } export async function saveLayouts(uid: string, layouts: savedLayout[]) { return saveUserData(uid, "savedLayouts", layouts); } export async function loadLayouts(uid: string) { - return loadUserData(uid, "savedLayouts", []); -} + return loadUserData(uid, "savedLayouts", []); +} \ No newline at end of file diff --git a/src/pages/Maker.tsx b/src/pages/Maker.tsx index 0e3aa2b..ae2ab7c 100644 --- a/src/pages/Maker.tsx +++ b/src/pages/Maker.tsx @@ -1,6 +1,5 @@ -import { useMemo, useState, useEffect } from "react"; -import { CourseCardProps, CoursesOnGrid, CoursesUsed } from "../types/types"; -import { mockCourses } from "../utils/dataImports"; +import { useState, useEffect } from "react"; +import { CourseCardProps } from "../types/types"; import { CourseForm, CourseGrid, @@ -8,13 +7,25 @@ import { SaveLayout, } from "../utils/componentImports"; import { UniqueIdentifier } from "@dnd-kit/core"; -import { emptyGrid } from "../utils/utilImports"; import { auth } from "../firebase/firebase"; -import { saveLayouts } from "../firebase/firestore"; +import { saveCourses, saveCoursesOnGrid, saveCoursesUsed, saveDependencies, saveLayouts } from "../firebase/firestore"; import { useLayoutContext } from "../components/layout/Layout"; const Maker = () => { - const { savedLayouts, setSavedLayouts } = useLayoutContext(); + const { + savedLayouts, + setSavedLayouts, + courses, + setCourses, + coursesUsed, + setCoursesUsed, + coursesOnGrid, + setCoursesOnGrid, + dependencies, + setDependencies, + } = useLayoutContext(); + // TODO: CHANGE THE ABOVE, probably don't need most of it because it's prop driling + // Course info for custom course const [customInfo, setCustomInfo] = useState({ @@ -33,67 +44,31 @@ const Maker = () => { const [preqString, setPreqString] = useState(""); const [coreqString, setCoreqString] = useState(""); - // Load courses from localStorage or use default values - const [courses, setCourses] = useState(() => { - const savedCourses = localStorage.getItem("courses"); - if (savedCourses) return JSON.parse(savedCourses); - return mockCourses; - }); - // Save courses to localStorage useEffect(() => { localStorage.setItem("courses", JSON.stringify(courses)); + const user = auth.currentUser; + if (user) saveCourses(user.uid, courses); }, [courses]); - // coursesUsed - const initialCoursesUsed = useMemo(() => { - const savedCoursesUsed = localStorage.getItem("coursesUsed"); - if (savedCoursesUsed) return JSON.parse(savedCoursesUsed); - - const posMap: CoursesUsed = {}; - Object.keys(courses).forEach((courseCode) => (posMap[courseCode] = "")); - return posMap; - }, [courses]); - const [coursesUsed, setCoursesUsed] = - useState(initialCoursesUsed); useEffect(() => { localStorage.setItem("coursesUsed", JSON.stringify(coursesUsed)); + const user = auth.currentUser; + if (user) saveCoursesUsed(user.uid, coursesUsed); }, [coursesUsed]); - // coursesOnGrid - const initialCoursesOnGrid = useMemo(() => { - const savedCoursesOnGrid = localStorage.getItem("coursesOnGrid"); - if (savedCoursesOnGrid) return JSON.parse(savedCoursesOnGrid); - return emptyGrid; - }, []); - const [coursesOnGrid, setCoursesOnGrid] = - useState(initialCoursesOnGrid); useEffect(() => { localStorage.setItem("coursesOnGrid", JSON.stringify(coursesOnGrid)); + const user = auth.currentUser; + if (user) saveCoursesOnGrid(user.uid, coursesOnGrid); }, [coursesOnGrid]); - // Map of co/prerequisites to their dependencies - const initialDependencies = useMemo< - Map> - >(() => { - const saved = localStorage.getItem("dependencies"); - if (!saved) { - return new Map(); - } - - try { - const parsed: [string, string[]][] = JSON.parse(saved); - return new Map(parsed.map(([key, values]) => [key, new Set(values)])); - } catch { - return new Map(); - } - }, []); - const [dependencies, setDependencies] = - useState>>(initialDependencies); useEffect(() => { const obj: [UniqueIdentifier, UniqueIdentifier[]][] = Array.from( dependencies.entries() ).map(([key, valueSet]) => [key, Array.from(valueSet)]); localStorage.setItem("dependencies", JSON.stringify(obj)); + const user = auth.currentUser; + if (user) saveDependencies(user.uid, dependencies); }, [dependencies]); useEffect(() => { diff --git a/src/types/types.tsx b/src/types/types.tsx index f46cff4..aba0f45 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -39,6 +39,25 @@ export interface MakerCardProps extends DraggableCardProps { setCoreqString: React.Dispatch>; }; +export interface LayoutContextType { + courses: CourseList; + setCourses: React.Dispatch>; + + coursesUsed: CoursesUsed; + setCoursesUsed: React.Dispatch>; + + coursesOnGrid: CoursesOnGrid; + setCoursesOnGrid: React.Dispatch>; + + dependencies: Map>; + setDependencies: React.Dispatch< + React.SetStateAction>> + >; + + savedLayouts: savedLayout[]; + setSavedLayouts: React.Dispatch>; +} + interface CourseIdentifierWithoutCode { name: string; preq?: (string | string[])[]; From 26d4b482fc551827eebd4a8d93ad8d1d23d8d19f Mon Sep 17 00:00:00 2001 From: Sunny Date: Fri, 10 Oct 2025 00:57:48 -0400 Subject: [PATCH 06/17] Eliminate prop drilling with useLayoutContext from CourseForm, LoadLayout, SaveLayout, and CourseGrid - Might also be able to eliminate other types, used by function returns --- src/components/forms/CourseForm.tsx | 8 +++++-- src/components/forms/LoadLayout.tsx | 22 +++++++++--------- src/components/forms/SaveLayout.tsx | 14 +++++++----- src/components/grid/CourseGrid.tsx | 18 +++++++++------ src/pages/Maker.tsx | 35 +++-------------------------- src/types/types.tsx | 26 --------------------- 6 files changed, 40 insertions(+), 83 deletions(-) diff --git a/src/components/forms/CourseForm.tsx b/src/components/forms/CourseForm.tsx index 4c07da2..e495fa5 100644 --- a/src/components/forms/CourseForm.tsx +++ b/src/components/forms/CourseForm.tsx @@ -9,10 +9,9 @@ import { import Announcement from "../info/Announcement"; import TextInput from "./TextInput"; import SubmitButton from "../SubmitButton"; +import { useLayoutContext } from "../layout/Layout"; const CourseForm: FC = ({ - setCourses, - setCoursesUsed, customInfo, setCustomInfo, preqString, @@ -20,6 +19,11 @@ const CourseForm: FC = ({ coreqString, setCoreqString, }) => { + const { + setCourses, + setCoursesUsed, + } = useLayoutContext(); + const [errors, setErrors] = useState({ code: false, name: false, diff --git a/src/components/forms/LoadLayout.tsx b/src/components/forms/LoadLayout.tsx index e017898..991cb2c 100644 --- a/src/components/forms/LoadLayout.tsx +++ b/src/components/forms/LoadLayout.tsx @@ -1,10 +1,10 @@ import { FC, useEffect, useRef, useState } from "react"; -import { LoadLayoutProps } from "../../types/types"; import { Announcement } from "../../utils/componentImports"; import { isValidString, parseString } from "../../utils/utilImports"; import Preset from "./Preset"; import TextInput from "./TextInput"; import SubmitButton from "../SubmitButton"; +import { useLayoutContext } from "../layout/Layout"; enum Load { NONE, @@ -12,15 +12,17 @@ enum Load { ERROR, } -const LoadLayout: FC = ({ - courses, - coursesUsed, - setCourses, - setCoursesOnGrid, - setCoursesUsed, - setDependencies, - savedLayouts, -}) => { +const LoadLayout: FC = () => { + const { + courses, + setCourses, + coursesUsed, + setCoursesUsed, + setCoursesOnGrid, + setDependencies, + savedLayouts, + } = useLayoutContext(); + const [str, setStr] = useState(""); const [load, setLoad] = useState(Load.NONE); const timeoutRef = useRef(); diff --git a/src/components/forms/SaveLayout.tsx b/src/components/forms/SaveLayout.tsx index 788aaa8..d6e98df 100644 --- a/src/components/forms/SaveLayout.tsx +++ b/src/components/forms/SaveLayout.tsx @@ -1,19 +1,21 @@ import { FC, useEffect, useRef, useState } from "react"; -import { SaveLayoutProps } from "../../types/types"; import { Announcement } from "../../utils/componentImports"; import TextInput from "./TextInput"; import SubmitButton from "../SubmitButton"; +import { useLayoutContext } from "../layout/Layout"; enum Save { NONE, SUCCESS, } -const SaveLayout: FC = ({ - courses, - coursesOnGrid, - setSavedLayouts, -}) => { +const SaveLayout: FC = () => { + const { + courses, + coursesOnGrid, + setSavedLayouts, + } = useLayoutContext(); + const [str, setStr] = useState(""); const [copied, setCopied] = useState(false); diff --git a/src/components/grid/CourseGrid.tsx b/src/components/grid/CourseGrid.tsx index 6ccdb63..53a684d 100644 --- a/src/components/grid/CourseGrid.tsx +++ b/src/components/grid/CourseGrid.tsx @@ -33,6 +33,7 @@ import { getYearTerm } from "../../utils/getYearTerm"; import Announcement from "../info/Announcement"; import { addDependencies } from "../../utils/utilImports"; import { emptyGrid } from "../../utils/utilImports"; +import { useLayoutContext } from "../layout/Layout"; enum DropError { NONE = "NONE", @@ -43,17 +44,20 @@ enum DropError { } const CourseGrid: FC = ({ - courses, - coursesOnGrid, - coursesUsed, - dependencies, - setCoursesOnGrid, - setCoursesUsed, setCustomInfo, setPreqString, setCoreqString, - setDependencies, }) => { + const { + courses, + coursesUsed, + setCoursesUsed, + coursesOnGrid, + setCoursesOnGrid, + dependencies, + setDependencies, + } = useLayoutContext(); + const [filters, setFilters] = useState({ searchTerm: "", streams: [], diff --git a/src/pages/Maker.tsx b/src/pages/Maker.tsx index ae2ab7c..9552755 100644 --- a/src/pages/Maker.tsx +++ b/src/pages/Maker.tsx @@ -13,19 +13,12 @@ import { useLayoutContext } from "../components/layout/Layout"; const Maker = () => { const { - savedLayouts, - setSavedLayouts, courses, - setCourses, coursesUsed, - setCoursesUsed, coursesOnGrid, - setCoursesOnGrid, dependencies, - setDependencies, + savedLayouts, } = useLayoutContext(); - // TODO: CHANGE THE ABOVE, probably don't need most of it because it's prop driling - // Course info for custom course const [customInfo, setCustomInfo] = useState({ @@ -80,16 +73,9 @@ const Maker = () => { return (

    @@ -98,29 +84,14 @@ const Maker = () => {

    Paste your previously copied string or load from cache

    - +

    Save layout

    Copy this string and save it for later or store layout in cache

    - +
    >; - setCoursesUsed: React.Dispatch>; - setCoursesOnGrid: React.Dispatch>; setCustomInfo: React.Dispatch>; setPreqString: React.Dispatch>; setCoreqString: React.Dispatch>; - setDependencies: React.Dispatch>>>; } export interface ParseString { @@ -148,23 +141,6 @@ export interface ParseString { dependencies: Map>; } -export interface LoadLayoutProps { - courses: CourseList; - coursesUsed: CoursesUsed; - setCourses: React.Dispatch>; - setCoursesUsed: React.Dispatch>; - setCoursesOnGrid: React.Dispatch>; - setDependencies: React.Dispatch>>>; - savedLayouts: savedLayout[]; -} - -export interface SaveLayoutProps { - courses: CourseList; - coursesOnGrid: CoursesOnGrid; - savedLayouts: savedLayout[]; - setSavedLayouts: React.Dispatch>; -} - export interface PresetProps { name: string; index: number; @@ -189,8 +165,6 @@ export interface TextInputProps { } export interface CourseFormProps { - setCourses: React.Dispatch>; - setCoursesUsed: React.Dispatch>; customInfo: CourseCardProps; setCustomInfo: React.Dispatch>; preqString: string; From b703ba0dfbc4524c3508cc6dd9484e75e251db53 Mon Sep 17 00:00:00 2001 From: Sunny Date: Fri, 10 Oct 2025 01:10:14 -0400 Subject: [PATCH 07/17] Change navbar height on scroll to h-14 instead of h-12 so elements stay vertically centered --- src/components/layout/Navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 9601142..a31b5be 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -34,7 +34,7 @@ const Navbar:FC = () => { return (