VYPR
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

Patches

1
020d2c4c1901

Merge 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

1

News mentions

0

No linked articles in our index yet.