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..1cd8eb9 --- /dev/null +++ b/src/components/SignIn.tsx @@ -0,0 +1,75 @@ +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 { loadCourses, loadCoursesOnGrid, loadCoursesUsed, loadDependencies, loadLayouts, loadTheme } from "../firebase/firestore"; + +const SignIn: FC = () => { + const { signedIn } = useAuth(); + + const { + setSavedLayouts, + setCourses, + setCoursesUsed, + setCoursesOnGrid, + setDependencies + } = useLayoutContext(); + + const signIn = async (e: React.MouseEvent) => { + e.preventDefault(); + const result = await signInGoogle(); + const user = result.user; + + try { + // Fetch everything in parallel + const [ + remoteSavedLayouts, + remoteCourses, + remoteCoursesUsed, + remoteCoursesOnGrid, + remoteDependencies, + remoteTheme, + ] = await Promise.all([ + loadLayouts(user.uid), + loadCourses(user.uid), + loadCoursesUsed(user.uid), + loadCoursesOnGrid(user.uid), + loadDependencies(user.uid), + loadTheme(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); + if (remoteTheme) { + if (remoteTheme == 'dark') { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } + } + + } catch (err) { + console.error("Failed to load user data:", err); + } + }; + + return ( +
+ +
+ ); +}; + +export default SignIn; diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index dac3942..8480e2a 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,3 +1,5 @@ +import { saveTheme } from "../firebase/firestore"; +import { auth } from "../firebase/firebase"; import { useDarkMode } from "../utils/utilImports"; import { CiLight, CiDark } from "react-icons/ci"; @@ -11,12 +13,14 @@ const ThemeToggle = () => { html.classList.toggle('dark'); localStorage.setItem('theme', isNowDark ? 'dark' : 'light'); + const user = auth.currentUser; + if (user) saveTheme(user.uid, isNowDark ? 'dark' : 'light'); }; return ( 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 e77d9c8..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(); @@ -96,7 +98,7 @@ const LoadLayout: FC = ({
{savedLayouts.map( (layout, index) => - layout && ( + layout?.name && ( = ({ - courses, - coursesOnGrid, - setSavedLayouts, -}) => { +const SaveLayout: FC = () => { + const { + courses, + coursesOnGrid, + setSavedLayouts, + } = useLayoutContext(); + const [str, setStr] = useState(""); const [copied, setCopied] = useState(false); @@ -35,8 +39,9 @@ const SaveLayout: FC = ({ useEffect(() => { let newStr = ""; - Object.entries(coursesOnGrid).forEach(([pos, courseCode]) => { - if (pos !== "3F.1" && pos.includes(".1")) newStr += "@@"; + Object.keys(emptyGrid).map((slot) => { + const courseCode: string = coursesOnGrid[slot as GridPositionBase]; + if (slot !== "3F.1" && slot.includes(".1")) newStr += "@@"; if (!courseCode) return; const course = courses[courseCode]; newStr += courseCode + course.name + "%%"; @@ -55,7 +60,7 @@ const SaveLayout: FC = ({ if (hasPreq) newStr += serializeReqs("p", course.preq); if (hasCoreq) newStr += serializeReqs(hasPreq ? "o" : "po", course.coreq); - if (pos[3] != "5") newStr += "$$"; + if (slot[3] != "5") newStr += "$$"; }); setStr(newStr); }, [coursesOnGrid, courses]); @@ -81,7 +86,14 @@ const SaveLayout: FC = ({ 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; }); @@ -164,7 +176,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..53a684d 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, @@ -32,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", @@ -42,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: [], @@ -500,27 +505,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 f8e005e..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 everything saved in local storage? + 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 01bc953..84f3292 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -1,23 +1,145 @@ -import { FC, ReactNode } from "react"; +import { + FC, + ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { Analytics } from "@vercel/analytics/react"; import { Footer, Navbar } from "../../utils/componentImports"; +import { CoursesOnGrid, CoursesUsed, LayoutContextType, savedLayout } from "../../types/types"; +import { UniqueIdentifier } from "@dnd-kit/core"; +import { emptyGrid } from "../../utils/emptyGrid"; +import mockCourses from "../../data/mockCourses"; +import { saveCourses, saveCoursesOnGrid, saveCoursesUsed, saveDependencies, saveLayouts } from "../../firebase/firestore"; +import { auth } from "../../firebase/firebase"; interface LayoutProps { children: ReactNode; } +// eslint-disable-next-line react-refresh/only-export-components +export const LayoutContext = createContext( + undefined +); + const Layout: FC = ({ children }) => { + + // courses + const [courses, setCourses] = useState(() => { + const savedCourses = localStorage.getItem("courses"); + if (savedCourses) return JSON.parse(savedCourses); + return mockCourses; + }); + 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 (note, dependants would have probably be more accurate) + 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]); + + // savedLayouts + const [savedLayouts, setSavedLayouts] = useState(() => { + const saved = localStorage.getItem("savedLayouts"); + return saved ? JSON.parse(saved) : []; + }); + useEffect(() => { + localStorage.setItem("savedLayouts", JSON.stringify(savedLayouts)); + const user = auth.currentUser; + if (user) saveLayouts(user.uid, savedLayouts); + }, [savedLayouts]); + return ( -
-
- -
- {children} - -
-
-
+ +
+
+ +
+ {children} + +
+
+
+ ); }; -export default Layout; +// eslint-disable-next-line react-refresh/only-export-components +export function useLayoutContext(): LayoutContextType { + const ctx = useContext(LayoutContext); + if (!ctx) + throw new Error("useLayoutContext must be used within LayoutProvider"); + return ctx; +} + +export default Layout; \ No newline at end of file diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index f3c095f..a31b5be 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); @@ -33,7 +34,7 @@ const Navbar:FC = () => { return (
) 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/data/faqData.tsx b/src/data/faqData.tsx index 415752d..44993fc 100644 --- a/src/data/faqData.tsx +++ b/src/data/faqData.tsx @@ -49,6 +49,14 @@ 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: "What should I do if my prerequisites aren't working or some info isn't consistent?", + answer: "To regenerate the dependencies of your courses on grid, you can save your current layout to one of the four saved layout slots, then immediately load that layout again. For inconsistent info or for trying to get updates, you may need to use the 'Reset all changes' button in the footer because the locally saved info is out of date (make sure to have a backup screenshot of your layout)." + }, { 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 new file mode 100644 index 0000000..a9cd561 --- /dev/null +++ b/src/firebase/auth.ts @@ -0,0 +1,20 @@ +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); + 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..86fb649 --- /dev/null +++ b/src/firebase/firebase.ts @@ -0,0 +1,22 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +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 auth = getAuth(app); +const db = getFirestore(app); + +export { app, auth, db }; + diff --git a/src/firebase/firestore.ts b/src/firebase/firestore.ts new file mode 100644 index 0000000..3d47e8d --- /dev/null +++ b/src/firebase/firestore.ts @@ -0,0 +1,143 @@ +import { doc, setDoc, getDoc } from "firebase/firestore"; +import { db } from "./firebase"; +import { CourseCardPropsWithoutCode, 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; +} + +// ========== SERIALIZE FOR FIRESTORE ========== + +// Encode preq/coreq so nested arrays become strings (Firestore-safe) +function encodeCourseRequirements(req?: (string | string[])[]): string[] { + if (!req) return []; + return req.map(r => 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 +function serializeCourses(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; + +function deserializeCourses(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; +} + +function serializeDependencies( + data: Map> +): Record { + return Object.fromEntries( + Array.from(data.entries(), ([key, set]) => [key, Array.from(set.values())]) + ); +} + +function deserializeDependencies( + data: Record +): Map> { + return new Map( + Object.entries(data).map(([key, arr]) => [key, new Set(arr)]) + ); +} + +// ========== DATA SAVE AND LOAD ========== + +export async function saveCourses(uid: string, courses: CourseList) { + return saveUserData(uid, "courses", serializeCourses(courses)); +} +export async function loadCourses(uid: string): Promise { + const raw = await loadUserData>(uid, "courses", {}); + return deserializeCourses(raw); +} + +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", {} as CoursesOnGrid); +} + +export async function saveDependencies( + uid: string, + deps: Map> +) { + return saveUserData(uid, "dependencies", serializeDependencies(deps)); +} +export async function loadDependencies(uid: string) { + const raw = await loadUserData(uid, "dependencies", {}); + return deserializeDependencies(raw); +} + +export async function saveLayouts(uid: string, layouts: savedLayout[]) { + return saveUserData(uid, "savedLayouts", layouts); +} +export async function loadLayouts(uid: string) { + return loadUserData(uid, "savedLayouts", []); +} + +export async function saveTheme(uid: string, theme: string) { + return saveUserData(uid, "theme", theme); +} +export async function loadTheme(uid: string) { + return loadUserData(uid, "theme", ""); +} \ No newline at end of file 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/pages/Maker.tsx b/src/pages/Maker.tsx index f3e6b3d..5289e75 100644 --- a/src/pages/Maker.tsx +++ b/src/pages/Maker.tsx @@ -1,16 +1,14 @@ -import { useMemo, useState, useEffect } from "react"; -import { CourseCardProps, CoursesOnGrid, CoursesUsed, savedLayout } from "../types/types"; -import { mockCourses } from "../utils/dataImports"; +import { useState } from "react"; +import { CourseCardProps } from "../types/types"; import { CourseForm, CourseGrid, LoadLayout, SaveLayout, } from "../utils/componentImports"; -import { UniqueIdentifier } from "@dnd-kit/core"; -import { emptyGrid } from "../utils/utilImports"; const Maker = () => { + // Course info for custom course const [customInfo, setCustomInfo] = useState({ code: "", @@ -28,126 +26,28 @@ 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)); - }, [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)); - }, [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)); - }, [coursesOnGrid]); - - // Map of co/prerequisites to their dependencies - const initialDependencies = useMemo>>(() => { - 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)); - }, [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)); - }, [savedLayouts]); - return (

-

- Load layout -

+

Load layout

Paste your previously copied string or load from cache

- -

- Save layout -

+ +

Save layout

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

- +
{ beforeEach(() => { localStorage.clear(); - render(); + render( + + + + ); stringInput = screen.getByPlaceholderText('Layout string'); loadLayout = screen.getByTestId('load-layout'); bucket = screen.getByTestId('bucket'); @@ -119,7 +124,7 @@ describe('Maker', () => { fireEvent.change(stringInput, { target: { value: mockLayouts.sampleGrad } }); fireEvent.click(loadLayout); expect(screen.getByText(/You graduate/)).toBeInTheDocument(); - expect(screen.getByText(/CE /)).toBeInTheDocument(); + expect(screen.getByText(/CE 🖥/)).toBeInTheDocument(); expect(stringInput).toHaveValue(''); }); @@ -359,7 +364,11 @@ describe('Maker', () => { // Simulate rerender cleanup(); - render(); + render( + + + + ); const newBucket = screen.getByTestId('bucket'); const newGrid = screen.getByTestId('grid'); diff --git a/src/types/types.tsx b/src/types/types.tsx index 1523062..a2dc18c 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; @@ -38,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[])[]; @@ -109,16 +129,9 @@ export interface fulfillsCoreqProps { } export interface CourseGridProps { - courses: CourseList; - coursesUsed: CoursesUsed; - coursesOnGrid: CoursesOnGrid; - dependencies: Map>; - setCoursesUsed: React.Dispatch>; - setCoursesOnGrid: React.Dispatch>; setCustomInfo: React.Dispatch>; setPreqString: React.Dispatch>; setCoreqString: React.Dispatch>; - setDependencies: React.Dispatch>>>; } export interface ParseString { @@ -128,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; @@ -169,8 +165,6 @@ export interface TextInputProps { } export interface CourseFormProps { - setCourses: React.Dispatch>; - setCoursesUsed: React.Dispatch>; customInfo: CourseCardProps; setCustomInfo: React.Dispatch>; preqString: string; @@ -189,4 +183,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 diff --git a/src/utils/useDarkMode.tsx b/src/utils/useDarkMode.tsx index 2f77b0c..50b63c9 100644 --- a/src/utils/useDarkMode.tsx +++ b/src/utils/useDarkMode.tsx @@ -1,9 +1,16 @@ import { useEffect, useState } from 'react'; export const useDarkMode = () => { - const [isDark, setIsDark] = useState(() => - document.documentElement.classList.contains('dark') - ); + const [isDark, setIsDark] = useState(() => { + const savedTheme = localStorage.getItem("theme"); + const html = document.documentElement; + + if (savedTheme == 'dark') { + html.classList.add('dark'); + return true; + } + return html.classList.contains('dark'); + }); useEffect(() => { const observer = new MutationObserver(() => {