High severityOSV Advisory· Published Sep 17, 2025· Updated Apr 15, 2026
CVE-2025-59416
CVE-2025-59416
Description
The Scratch Channel is a news website. If the user makes a fork, they can change the admins and make an article. Since the API uses a POST request, it will make an article. This issue is fixed in v1.2.
Affected products
1- Range: beta1, v1, v1.1
Patches
1020d2c4c1901Merge pull request #84 from The-Scratch-Channel/firebase
11 files changed · +1587 −364
package.json+10 −9 modified@@ -10,18 +10,19 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.14.1", - "sanitize-html": "^2.17.0", - "marked": "^16.1.1", - "@tiptap/react": "^2.10.0", - "@tiptap/starter-kit": "^2.10.0", "@tiptap/extension-bold": "^2.10.0", + "@tiptap/extension-image": "^2.10.0", "@tiptap/extension-italic": "^2.10.0", - "@tiptap/extension-underline": "^2.10.0", "@tiptap/extension-link": "^2.10.0", - "@tiptap/extension-image": "^2.10.0" + "@tiptap/extension-underline": "^2.10.0", + "@tiptap/react": "^2.10.0", + "@tiptap/starter-kit": "^2.10.0", + "firebase": "^12.1.0", + "marked": "^16.1.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.1", + "sanitize-html": "^2.17.0" }, "devDependencies": { "@eslint/js": "^9.30.1",
package-lock.json+1029 −4 modified@@ -15,6 +15,7 @@ "@tiptap/extension-underline": "^2.10.0", "@tiptap/react": "^2.10.0", "@tiptap/starter-kit": "^2.10.0", + "firebase": "^12.1.0", "marked": "^16.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -925,6 +926,645 @@ "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/@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", @@ -1040,6 +1680,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/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -1851,6 +2555,15 @@ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1946,11 +2659,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2067,11 +2788,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2084,7 +2818,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -2225,6 +2958,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2283,7 +3022,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" @@ -2499,6 +3237,18 @@ "dev": true, "license": "MIT" }, + "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/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -2544,6 +3294,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", @@ -2590,6 +3400,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2645,6 +3464,18 @@ "entities": "^4.4.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/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2692,6 +3523,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2842,13 +3682,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "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", @@ -3305,6 +4157,30 @@ "prosemirror-transform": "^1.1.0" } }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3391,6 +4267,15 @@ "react-dom": ">=16.8" } }, + "node_modules/require-directory": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3447,6 +4332,26 @@ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", "license": "MIT" }, + "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/sanitize-html": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz", @@ -3512,6 +4417,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3564,6 +4495,12 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3583,6 +4520,12 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3714,6 +4657,35 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "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/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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3740,13 +4712,66 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
src/App.jsx+42 −2 modified@@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Header from "./components/Header"; import MainContent from "./pages/MainContent"; @@ -7,6 +7,11 @@ import CreateArticle from "./pages/createArticles"; import About from "./pages/About"; import ArticlePage from "./pages/ArticlePage"; import LoginPage from "./pages/Login"; +import Account from "./pages/Account"; +import SignUpForm from "./pages/SignUp"; +import { auth, db } from "./firebaseConfig"; +import { onAuthStateChanged } from "firebase/auth"; +import { doc, getDoc } from "firebase/firestore"; import "./styles/main.css"; import "./styles/about.css"; @@ -19,15 +24,50 @@ import "./styles/categories.css"; import "./styles/editor.css"; function App() { + const [user, setUser] = useState(null); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => { + if (firebaseUser) { + setUser(firebaseUser); + const userDoc = await getDoc(doc(db, "users", firebaseUser.uid)); + if (userDoc.exists()) { + setProfile(userDoc.data()); + } + } else { + setUser(null); + setProfile(null); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + if (loading) return <p>Loading...</p>; + return ( <Router> <Header /> <Routes> <Route path="/" element={<MainContent />} /> - <Route path="/articles/create" element={<CreateArticle />} /> + <Route + path="/articles/create" + element={ + user && profile?.writer ? ( + <CreateArticle user={user} profile={profile} /> + ) : ( + <p>Not authorized</p> + ) + } + /> <Route path="/about" element={<About />} /> <Route path="/:category/article/:filename" element={<ArticlePage />} /> + <Route path="/account" element={<Account />} /> <Route path="/login" element={<LoginPage />} /> + <Route path="/signup" element={<SignUpForm />} /> </Routes> <Footer /> </Router>
src/components/Header.jsx+1 −1 modified@@ -35,7 +35,7 @@ export default function Header() { > <i className={darkMode ? "fa-solid fa-sun" : "fa-solid fa-moon"} /> </button> - <Link to="/login"> + <Link to="/account"> Account </Link> </div>
src/firebaseConfig.js+17 −0 added@@ -0,0 +1,17 @@ +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + // Your project's Firebase configuration object goes here + apiKey: "AIzaSyAeYmYKhCj08KubXcs-wACuAk9LrL1Weyk", + authDomain: "the-scratch-channel.firebaseapp.com", + projectId: "the-scratch-channel", + storageBucket: "the-scratch-channel.firebasestorage.app", + messagingSenderId: "626573218185", + appId: "1:626573218185:web:185fe5f77ea45c5e831158" +}; + +const app = initializeApp(firebaseConfig); +export const db = getFirestore(app); +export const auth = getAuth(app); \ No newline at end of file
src/pages/Account.jsx+47 −0 added@@ -0,0 +1,47 @@ +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { auth } from "../firebaseConfig"; +import { onAuthStateChanged, signOut } from "firebase/auth"; + +export default function Account() { + const [user, setUser] = useState(null); + + useEffect(() => { + // listen for auth state changes any change causes thiz + const unsubscribe = onAuthStateChanged(auth, (currentUser) => { + setUser(currentUser); + }); + + // sign out and remove local data from browserz + return () => unsubscribe(); + }, []); + + const handleLogout = async () => { + try { + await signOut(auth); + alert("Logged out successfully"); + } catch (error) { + console.error("Error logging out:", error.message); + alert(`Error logging out: ${error.message}`); + } + }; + + return ( + <div style={{ padding: "20px", textAlign: "center" }}> + {user ? ( + <> + <p>Welcome, {user.email}!</p> + <button onClick={handleLogout}>Log out</button> + </> + ) : ( + <> + <p>What would you like to do?</p> + <div style={{ marginTop: "10px" }}> + <Link to="/login" style={{ marginRight: "15px" }}>Log in</Link> + <Link to="/signup">Sign up</Link> + </div> + </> + )} + </div> + ); +}
src/pages/ArticlePage.jsx+135 −91 modified@@ -1,102 +1,146 @@ import React, { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { marked } from "marked"; -import sanitizeHtml from "sanitize-html"; +import { useParams } from "react-router-dom"; +import { db, auth } from "../firebaseConfig"; +import { doc, getDoc, updateDoc, increment, setDoc } from "firebase/firestore"; +import { onAuthStateChanged } from "firebase/auth"; export default function ArticlePage() { - const { filename, category } = useParams(); - const [article, setArticle] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function fetchArticle() { - try { - const fileRes = await fetch(`https://corsproxy.io/?url=https://raw.githubusercontent.com/The-Scratch-Channel/the-scratch-channel.github.io/refs/heads/main/AUTOADDED-ARTICLES/${filename}`); - const text = await fileRes.text(); - const lines = text.split("\n"); - - if (lines.length < 3) { - console.warn(`Skipping ${filename}: not enough lines`); - return; - } - - const metadataRow = lines[2].trim(); - if (!metadataRow.startsWith("|") || !metadataRow.endsWith("|")) { - console.warn(`Skipping ${filename}: invalid metadata row`); - return; - } - - const metadataValues = metadataRow - .split("|") - .map(s => s.trim()) - .filter(s => s.length > 0); - - if (metadataValues.length < 3) { - console.warn(`Skipping ${filename}: not enough metadata`); - return; - } - - const [title, author, date] = metadataValues; - const contentStartIndex = lines.findIndex((line, i) => i > 2 && line.trim() !== ""); - - if (contentStartIndex === -1) { - console.warn(`Skipping ${filename}: no content`); - return; - } - - const content = await marked.parse(lines.slice(contentStartIndex).join("\n")); - - let thumbnail = null; - const imgRegex = /<img[^>]+src="([^">]+)"/; - const imgMatch = content.match(imgRegex); - if (imgMatch && imgMatch[1]) { - thumbnail = imgMatch[1]; - } - - setArticle({ - title, - author, - date, - content, - filename, - thumbnail - }); - } catch (error) { - console.error("Failed to fetch article:", error); - } finally { - setLoading(false); - } - } + const { filename, category } = useParams(); + const [article, setArticle] = useState(null); + const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + const [userReactions, setUserReactions] = useState({ thumbsUp: false, thumbsDown: false, heart: false }); + const [animate, setAnimate] = useState({ thumbsUp: false, thumbsDown: false, heart: false }); + const [reactions, setReactions] = useState({ thumbsUp: 0, thumbsDown: 0, heart: 0 }); + + useEffect(() => { + onAuthStateChanged(auth, (u) => { + setUser(u); + }); + }, []); - fetchArticle(); - }, [filename]); + useEffect(() => { + async function fetchArticle() { + const docRef = doc(db, "articles", filename); + const docSnap = await getDoc(docRef); + if (!docSnap.exists()) { + setArticle(null); + } else { + const data = docSnap.data(); + setArticle(data); + setReactions({ + thumbsUp: data.thumbsUp || 0, + thumbsDown: data.thumbsDown || 0, + heart: data.heart || 0, + }); - if (loading) { - return <div>Loading article...</div>; + if (user) { + const userDocRef = doc(db, "articles", filename, "reactions", user.uid); + const userDoc = await getDoc(userDocRef); + if (userDoc.exists()) { + setUserReactions(userDoc.data()); + } + } + } + setLoading(false); } + fetchArticle(); + }, [filename, user]); + + const handleReaction = async (type) => { + if (!user) return; + + const articleRef = doc(db, "articles", filename); + const userDocRef = doc(db, "articles", filename, "reactions", user.uid); - if (!article) { - return <div>Article not found</div>; + // If user already reacted, remove the reaction (decrement) + if (userReactions[type]) { + setAnimate((prev) => ({ ...prev, [type]: true })); + setTimeout(() => setAnimate((prev) => ({ ...prev, [type]: false })), 200); + + await updateDoc(articleRef, { [type]: increment(-1) }); + await setDoc(userDocRef, { ...userReactions, [type]: false }, { merge: true }); + + setReactions((prev) => ({ ...prev, [type]: Math.max(0, prev[type] - 1) })); + setUserReactions((prev) => ({ ...prev, [type]: false })); + return; } - return ( - <div className="page article-full"> - <div className="article-header"> - <h1>{article.title}</h1> - <div className="meta"> - <span className="author">By: {article.author}</span> - <span className="date">Date: {article.date}</span> - </div> - </div> - {article.thumbnail && ( - <div className="article-thumbnail"> - <img src={article.thumbnail} alt="Article thumbnail" /> - </div> - )} - <div - className="article-full-content" - dangerouslySetInnerHTML={{ __html: article.content }} - /> + // Otherwise add the reaction (increment) + setAnimate((prev) => ({ ...prev, [type]: true })); + setTimeout(() => setAnimate((prev) => ({ ...prev, [type]: false })), 200); + + await updateDoc(articleRef, { [type]: increment(1) }); + await setDoc(userDocRef, { ...userReactions, [type]: true }, { merge: true }); + + setReactions((prev) => ({ ...prev, [type]: prev[type] + 1 })); + setUserReactions((prev) => ({ ...prev, [type]: true })); + }; + + if (loading) return <div>Loading article...</div>; + if (!article) return <div>Article not found</div>; + if (article.category !== category) return <div>Category mismatch</div>; + + return ( + <div className="page article-full"> + <div className="article-header"> + <h1>{article.title}</h1> + <div className="meta"> + <span className="author">By: {article.author}</span> + <span className="date">Date: {article.date}</span> + <span className="category">Category: {article.category}</span> + </div> + </div> + + {article.thumbnail && ( + <div className="article-thumbnail"> + <img src={article.thumbnail} alt="Article thumbnail" /> </div> - ); + )} + + <div className="article-full-content" dangerouslySetInnerHTML={{ __html: article.content }} /> + + <div className="reactions"> + <button + className={`reaction-btn ${animate.thumbsUp ? "animate" : ""}`} + onClick={() => handleReaction("thumbsUp")} + style={{ color: userReactions.thumbsUp ? "#0d6efd" : "grey" }} + > + 👍 {reactions.thumbsUp} + </button> + <button + className={`reaction-btn ${animate.thumbsDown ? "animate" : ""}`} + onClick={() => handleReaction("thumbsDown")} + style={{ color: userReactions.thumbsDown ? "#dc3545" : "grey" }} + > + 👎 {reactions.thumbsDown} + </button> + <button + className={`reaction-btn ${animate.heart ? "animate" : ""}`} + onClick={() => handleReaction("heart")} + style={{ color: userReactions.heart ? "#ff4081" : "grey" }} + > + ❤️ {reactions.heart} + </button> + </div> + + <style>{` + .reactions { + display: flex; + gap: 12px; + margin-top: 20px; + } + .reaction-btn { + font-size: 24px; + background: none; + border: none; + cursor: pointer; + transition: transform 0.2s; + } + .reaction-btn.animate { + transform: scale(1.4); + } + `}</style> + </div> + ); }
src/pages/createArticles.jsx+53 −62 modified@@ -1,93 +1,74 @@ -import React, { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; import { EditorContent, useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Bold from "@tiptap/extension-bold"; import Italic from "@tiptap/extension-italic"; import Underline from "@tiptap/extension-underline"; import Link from "@tiptap/extension-link"; import Image from "@tiptap/extension-image"; +import { db } from "../firebaseConfig"; +import { collection, addDoc, serverTimestamp } from "firebase/firestore"; -export default function CreateArticle() { +export default function CreateArticle({ user, profile }) { const navigate = useNavigate(); - const [scratchUser, setScratchUser] = useState(null); - const [loading, setLoading] = useState(true); + const { category: routeCategory } = useParams(); + const [title, setTitle] = useState(""); const [date] = useState(new Date().toISOString().split("T")[0]); - const allowedAdmins = ["SmartCat3","Swiftpixel","scratchcode1_2_3","kRxZy_kRxZy","GvYoutube","snoopythe3"]; const categories = ["TSC Announcements", "TSC Update Log", "Scratch News"]; - const [category, setCategory] = useState(categories[0]); + const [category, setCategory] = useState( + categories.includes(routeCategory) ? routeCategory : categories[0] + ); const editor = useEditor({ extensions: [StarterKit, Bold, Italic, Underline, Link, Image], content: "", }); - useEffect(() => { - const token = localStorage.getItem("scratchToken"); - if (!token) { - setLoading(false); - return; - } - - fetch(`https://corsproxy.io/?url=https://scratch-id.onrender.com/verification/${token}`) - .then(res => res.json()) - .then(data => { - const sessionKey = Object.keys(data)[0]; - if (sessionKey && data[sessionKey]) { - setScratchUser(data[sessionKey].user); - } - }) - .catch(err => console.error("Auth error:", err)) - .finally(() => setLoading(false)); - }, []); - - if (loading) return <div>Loading...</div>; - if (!scratchUser) return <div className="container mt-4 alert alert-warning">⚠️ You must be logged in to post.</div>; - if (!allowedAdmins.includes(scratchUser)) return <div className="container mt-4 alert alert-danger">🚫 Only admins can create posts.</div>; + if (!profile?.writer) return <p>Not authorized to create articles</p>; const handleSubmit = async () => { - if (!title) return alert("Title is required."); - + if (!title) return alert("Title is required"); const content = editor.getHTML(); - const [year, month, day] = date.split("-"); - const formattedDate = `${day}/${month}/${year.slice(2)}`; - - const fileContent = `| Title | Author | Date | Category | -|-------|--------|------|----------| -| ${title} | ${scratchUser} | ${formattedDate} | ${category} | - -${content} -`; - const file = new File([fileContent], `${title.replace(/\s+/g, "_")}.md`, { type: "text/markdown" }); - const formData = new FormData(); - formData.append("file", file); + await addDoc(collection(db, "articles"), { + title, + author: profile.username, + date, + category, + content, + createdAt: serverTimestamp(), + }); - try { - const response = await fetch("https://myscratchblocks.onrender.com/the-scratch-channel/articles/create", { method: "POST", body: formData }); - if (!response.ok) throw new Error(`Server error: ${response.statusText}`); - alert("✅ Article submitted successfully!"); - navigate("/"); - } catch (error) { - console.error("Error submitting article:", error); - alert("❌ Failed to submit article. Please try again."); - } + navigate("/"); }; return ( <div className="container mt-4"> <div className="card shadow p-4"> - <h1 className="mb-4">✍️ Create Article</h1> + <h1 className="mb-4">Create Article</h1> <div className="mb-3"> <label className="form-label">Title</label> - <input className="form-control" type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Enter your article title..." /> + <input + className="form-control" + type="text" + value={title} + onChange={(e) => setTitle(e.target.value)} + placeholder="Enter your article title..." + /> </div> <div className="mb-3"> <label className="form-label">Author</label> - <input className="form-control" type="text" value={scratchUser} readOnly disabled /> + <input + className="form-control" + type="text" + value={profile.username} + readOnly + disabled + /> </div> <div className="mb-3"> @@ -97,21 +78,31 @@ ${content} <div className="mb-3"> <label className="form-label">Category</label> - <select className="form-select" value={category} onChange={e => setCategory(e.target.value)}> - {categories.map(cat => <option key={cat} value={cat}>{cat}</option>)} + <select + className="form-select" + value={category} + onChange={(e) => setCategory(e.target.value)} + > + {categories.map((cat) => ( + <option key={cat} value={cat}> + {cat} + </option> + ))} </select> </div> <div className="mb-3"> <label className="form-label">Content</label> - <div className="editor-container"> - <EditorContent editor={editor} /> - </div> + <EditorContent editor={editor} /> </div> <div className="d-flex gap-3 mt-3"> - <button className="btn btn-primary" onClick={handleSubmit}>Submit</button> - <button className="btn btn-secondary" onClick={() => navigate("/")}>Cancel</button> + <button className="btn btn-primary" onClick={handleSubmit}> + Submit + </button> + <button className="btn btn-secondary" onClick={() => navigate("/")}> + Cancel + </button> </div> </div> </div>
src/pages/Login.jsx+35 −69 modified@@ -1,80 +1,46 @@ -import { useEffect, useState } from "react"; +import React, { useState } from "react"; +import { auth } from "../firebaseConfig"; +import { signInWithEmailAndPassword } from "firebase/auth"; +import { useNavigate } from "react-router-dom"; export default function LoginPage() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const REDIRECT_URI = window.location.origin + "/login"; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const tokenFromUrl = params.get("id"); - const savedToken = localStorage.getItem("scratchToken"); + const handleSignIn = async (e) => { + e.preventDefault(); - const token = tokenFromUrl || savedToken; - - if (token) { - localStorage.setItem("scratchToken", token); - fetch(`https://corsproxy.io/?url=https://scratch-id.onrender.com/verification/${token}`) - .then(res => res.json()) - .then(data => { - const sessionKey = Object.keys(data)[0]; - if (sessionKey && data[sessionKey]) { - const username = data[sessionKey].user; - fetch(`https://corsproxy.io/?url=https://api.scratch.mit.edu/users/${username}`) - .then(res => res.json()) - .then(userInfo => { - const userObj = { - username: userInfo.username, - avatarUrl: `https://uploads.scratch.mit.edu/get_image/user/${userInfo.id}_500x500.png`, - }; - setUser(userObj); - localStorage.setItem("scratchUser", JSON.stringify(userObj)); - }); - } - }) - .catch(err => console.error("Auth error:", err)) - .finally(() => setLoading(false)); - } else { - setLoading(false); + if (!email || !password) { + alert("Please enter both email and password."); + return; } - }, []); - - const handleLogin = () => { - const redirectParam = btoa(REDIRECT_URI); - window.location.href = `https://scratch-id.onrender.com/?redirect=${redirectParam}&name=${encodeURIComponent( - "The Scratch Channel" - )}`; - }; - const handleLogout = () => { - setUser(null); - localStorage.removeItem("scratchUser"); - localStorage.removeItem("scratchToken"); + try { + await signInWithEmailAndPassword(auth, email, password); + console.log(`User ${email} signed in successfully`); + navigate('/'); + } catch (error) { + console.error("Error signing in", error.message); + alert(`Error signing in: ${error.message}`); + } }; - if (loading) return <div style={{ textAlign: "center", marginTop: "2rem" }}>Loading...</div>; - return ( - <div style={{ display: "flex", height: "100vh", alignItems: "center", justifyContent: "center", backgroundColor: "#f3f4f6" }}> - <div style={{ backgroundColor: "white", padding: "2rem", borderRadius: "1rem", boxShadow: "0 4px 12px rgba(0,0,0,0.1)", width: "22rem", textAlign: "center" }}> - {!user ? ( - <> - <h1 style={{ fontSize: "1.25rem", fontWeight: "bold", marginBottom: "1rem" }}>Login with Scratch</h1> - <button onClick={handleLogin} style={{ width: "100%", padding: "0.75rem", backgroundColor: "#4f46e5", color: "white", fontWeight: "bold", border: "none", borderRadius: "0.75rem", cursor: "pointer" }}> - Login with Scratch - </button> - </> - ) : ( - <> - <h1 style={{ fontSize: "1.25rem", fontWeight: "bold", marginBottom: "0.5rem" }}>Welcome, {user.username}!</h1> - {user.avatarUrl && <img src={user.avatarUrl} alt="avatar" style={{ borderRadius: "50%", width: "6rem", height: "6rem", margin: "0 auto" }} />} - <p style={{ marginTop: "0.75rem", color: "#4b5563" }}>Welcome To Your Account!</p> - <button onClick={handleLogout} style={{ marginTop: "1rem", padding: "0.5rem 1rem", backgroundColor: "#dc2626", color: "white", border: "none", borderRadius: "0.5rem", cursor: "pointer" }}> - Logout - </button> - </> - )} - </div> - </div> + <form onSubmit={handleSignIn}> + <input + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="Email" + /> + <input + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Password" + /> + <button type="submit">Sign In</button> + </form> ); }
src/pages/MainContent.jsx+159 −126 modified@@ -1,139 +1,172 @@ -import React, { useEffect, useState, useRef } from "react"; -import { marked } from "marked"; -import sanitizeHtml from "sanitize-html"; +import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { db, auth } from "../firebaseConfig"; +import { collection, getDocs, doc, getDoc, updateDoc, increment, setDoc } from "firebase/firestore"; +import { onAuthStateChanged } from "firebase/auth"; export default function MainContent() { - const [categories, setCategories] = useState([]); - const [articlesByCategory, setArticlesByCategory] = useState({}); - const [selectedCategory, setSelectedCategory] = useState(null); - const navigate = useNavigate(); - const articleID = useRef({}); - - const folder = "https://corsproxy.io/?url=https://raw.githubusercontent.com/The-Scratch-Channel/the-scratch-channel.github.io/refs/heads/main/AUTOADDED-ARTICLES"; - - useEffect(() => { - async function fetchArticles() { - try { - const res = await fetch(`${folder}/index.json`); - const files = await res.json(); - - const articles = await Promise.all( - files.map(async (file) => { - const articleRes = await fetch(`${folder}/${file}`); - const text = await articleRes.text(); - const lines = text.split("\n"); - - if (lines.length < 3) return null; - - // Extract metadata (title, author, date, category) - const metadataRow = lines[2].trim(); - if (!metadataRow.startsWith("|") || !metadataRow.endsWith("|")) return null; - const metadataValues = metadataRow.split("|").map(s => s.trim()).filter(s => s.length > 0); - if (metadataValues.length < 4) return null; - - const [title, author, date, category] = metadataValues; - articleID.current[title] = file; - - const contentStartIndex = lines.findIndex((line, i) => i > 2 && line.trim() !== ""); - if (contentStartIndex === -1) return null; - - const contentHtml = marked.parse(lines.slice(contentStartIndex).join("\n")); - const textContent = sanitizeHtml(contentHtml, { allowedTags: [], allowedAttributes: {} }); - - const words = textContent.split(/\s+/); - const preview = words.length > 25 ? words.slice(0, 25).join(" ") + "..." : textContent; - - let thumbnail = null; - const imgMatch = contentHtml.match(/<img[^>]+src="([^">]+)"/); - if (imgMatch && imgMatch[1]) thumbnail = imgMatch[1]; - - return { filename: file, title, author, date, category, preview, thumbnail }; - }) - ); - - const validArticles = articles.filter(Boolean); - - // Build category list and group articles - const grouped = { - "TSC Announcements": [], - "TSC Update Log": [], - "Scratch News": [] - }; - validArticles.forEach(article => { - if (!grouped[article.category]) grouped[article.category] = []; - grouped[article.category].push(article); - }); - - setCategories(Object.keys(grouped)); - setArticlesByCategory(grouped); - } catch (err) { - console.error("Error fetching articles:", err); - } + const [categories, setCategories] = useState([]); + const [articlesByCategory, setArticlesByCategory] = useState({}); + const [selectedCategory, setSelectedCategory] = useState(null); + const [user, setUser] = useState(null); + const [userReactions, setUserReactions] = useState({}); + const [animate, setAnimate] = useState({}); + const navigate = useNavigate(); + + useEffect(() => { + onAuthStateChanged(auth, (u) => setUser(u)); + }, []); + + useEffect(() => { + async function fetchArticles() { + const snapshot = await getDocs(collection(db, "articles")); + const grouped = {}; + + for (let docSnap of snapshot.docs) { + const data = docSnap.data(); + const id = docSnap.id; + const article = { + id, + title: data.title, + author: data.author, + date: data.date, + category: data.category, + preview: data.preview || "", + thumbnail: data.thumbnail || "", + reactions: { + thumbsUp: data.thumbsUp || 0, + thumbsDown: data.thumbsDown || 0, + heart: data.heart || 0, + }, + }; + + if (!grouped[article.category]) grouped[article.category] = []; + grouped[article.category].push(article); + + // fetch user reactions if logged in + if (user) { + const userDocRef = doc(db, "articles", id, "reactions", user.uid); + const userDoc = await getDoc(userDocRef); + setUserReactions((prev) => ({ + ...prev, + [id]: userDoc.exists() ? userDoc.data() : { thumbsUp: false, thumbsDown: false, heart: false }, + })); } + } - fetchArticles(); - }, []); - - const openArticle = (article) => { - navigate(`${selectedCategory}/article/${article.filename}`); - }; - - if (!selectedCategory) { - // Display categories with article counts - return ( - <div className="page"> - <h1 style={{ textAlign: "center" }}>Welcome to The Scratch Channel!</h1> - <p style={{ textAlign: "center" }}>Click a category to view its articles.</p> - <div className="categories-container"> - {categories.map((cat, idx) => ( - <div - key={idx} - className="category-card" - onClick={() => setSelectedCategory(cat)} - > - {cat} ({articlesByCategory[cat]?.length || 0}) - </div> - ))} - </div> - </div> - ); + setCategories(Object.keys(grouped)); + setArticlesByCategory(grouped); } - // Display articles in selected category - const articles = articlesByCategory[selectedCategory] || []; + fetchArticles(); + }, [user]); + + const handleReaction = async (articleId, type) => { + if (!user) return; + if (userReactions[articleId]?.[type]) return; + + setAnimate((prev) => ({ ...prev, [articleId]: { ...(prev[articleId] || {}), [type]: true } })); + setTimeout(() => setAnimate((prev) => ({ ...prev, [articleId]: { ...(prev[articleId] || {}), [type]: false } })), 200); + + const articleRef = doc(db, "articles", articleId); + await updateDoc(articleRef, { [type]: increment(1) }); + + const userDocRef = doc(db, "articles", articleId, "reactions", user.uid); + await setDoc(userDocRef, { ...(userReactions[articleId] || {}), [type]: true }, { merge: true }); + + setUserReactions((prev) => ({ + ...prev, + [articleId]: { ...(prev[articleId] || {}), [type]: true }, + })); + setArticlesByCategory((prev) => { + const updated = { ...prev }; + for (let cat in updated) { + updated[cat] = updated[cat].map(a => a.id === articleId ? { ...a, reactions: { ...a.reactions, [type]: a.reactions[type] + 1 } } : a); + } + return updated; + }); + }; + + if (!selectedCategory) { return ( - <div className="page"> - <h1 style={{ textAlign: "center" }}> - {selectedCategory} - </h1> - <button className="back-btn" onClick={() => setSelectedCategory(null)}>← Back to Categories</button> - - <div className="articles-container"> - {articles.map((article, index) => ( - <div key={index} className="article-card"> - {article.thumbnail && ( - <div className="card-thumbnail"> - <img src={article.thumbnail} alt="Article thumbnail" loading="lazy" /> - </div> - )} - <div className="card-header"> - <h3>{article.title}</h3> - <div className="meta"> - <span className="author">By: {article.author}</span> - <span className="date">Date: {article.date}</span> - </div> - </div> - <div className="card-content"> - <p>{article.preview}</p> - </div> - <div className="read-more" onClick={() => openArticle(article)}> - Read More → - </div> - </div> - ))} + <div className="page"> + <h1 style={{ textAlign: "center" }}>Welcome to The Scratch Channel!</h1> + <div className="categories-container"> + {categories.map((cat) => ( + <div key={cat} className="category-card" onClick={() => setSelectedCategory(cat)}> + {cat} ({articlesByCategory[cat]?.length || 0}) </div> + ))} </div> + </div> ); + } + + const articles = articlesByCategory[selectedCategory] || []; + + return ( + <div className="page"> + <h1 style={{ textAlign: "center" }}>{selectedCategory}</h1> + <button className="back-btn" onClick={() => setSelectedCategory(null)}>← Back to Categories</button> + + <div className="articles-container"> + {articles.map((article) => ( + <div key={article.id} className="article-card"> + {article.thumbnail && <div className="card-thumbnail"><img src={article.thumbnail} alt="" loading="lazy" /></div>} + <div className="card-header"> + <h3>{article.title}</h3> + <div className="meta"> + <span className="author">By: {article.author}</span> + <span className="date">Date: {article.date}</span> + </div> + </div> + <div className="card-content"><p>{article.preview}</p></div> + <div className="reactions"> + <button + className={`reaction-btn ${animate[article.id]?.thumbsUp ? "animate" : ""}`} + onClick={() => handleReaction(article.id, "thumbsUp")} + style={{ color: userReactions[article.id]?.thumbsUp ? "#0d6efd" : "grey" }} + > + 👍 {article.reactions.thumbsUp} + </button> + <button + className={`reaction-btn ${animate[article.id]?.thumbsDown ? "animate" : ""}`} + onClick={() => handleReaction(article.id, "thumbsDown")} + style={{ color: userReactions[article.id]?.thumbsDown ? "#dc3545" : "grey" }} + > + 👎 {article.reactions.thumbsDown} + </button> + <button + className={`reaction-btn ${animate[article.id]?.heart ? "animate" : ""}`} + onClick={() => handleReaction(article.id, "heart")} + style={{ color: userReactions[article.id]?.heart ? "#ff4081" : "grey" }} + > + ❤️ {article.reactions.heart} + </button> + </div> + <div className="read-more" onClick={() => navigate(`${selectedCategory}/article/${article.id}`)}>Read More →</div> + </div> + ))} + </div> + + <style>{` + .reactions { + display: flex; + gap: 12px; + margin-top: 10px; + } + .reaction-btn { + font-size: 20px; + background: none; + border: none; + cursor: pointer; + transition: transform 0.2s; + } + .reaction-btn.animate { + transform: scale(1.4); + } + `}</style> + </div> + ); }
src/pages/SignUp.jsx+59 −0 added@@ -0,0 +1,59 @@ +import React, { useState } from "react"; +import { auth } from "../firebaseConfig"; +import { createUserWithEmailAndPassword } from "firebase/auth"; +import { doc, setDoc } from "firebase/firestore"; +import { db } from "../firebaseConfig"; + +export default function SignUpForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [username, setUsername] = useState(''); + + const handleSignUp = async (e) => { + e.preventDefault(); + try { + // create user + const userCredential = await createUserWithEmailAndPassword(auth, email, password); + const user = userCredential.user; + + // usernam functionality using firestore + await setDoc(doc(db, "users", user.uid), { + username: username, + email: user.email, + createdAt: new Date(), + writer: false + }) + console.log("user registered", user); + + } catch (error) { + console.error("error signing up", error.message); + } + }; + return ( + <form onSubmit={handleSignUp}> + <h2>Sign Up with Username</h2> + <input + type="email" + value={email} + onChange={(e) => setEmail(e.target.value)} + placeholder="Email" + required + /> + <input + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Password" + required + /> + <input + type="text" + value={username} + onChange={(e) => setUsername(e.target.value)} + placeholder="Username" + required + /> + <button type="submit">Register</button> + </form> + ) +} \ No newline at end of file
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.