From 2c65f0164d015d3366d2bd6f7380ab15f88af765 Mon Sep 17 00:00:00 2001 From: Riley G Date: Sat, 25 Apr 2026 19:32:23 -0600 Subject: [PATCH 1/4] feat: add Firebase Google sign-in and cloud sync --- .env.example | 6 + .github/workflows/pages-release.yml | 7 + docs/FIREBASE_SETUP.md | 51 ++ package-lock.json | 1066 ++++++++++++++++++++++++++- package.json | 1 + src/App.tsx | 244 ++++-- src/firebase.ts | 21 + src/vite-env.d.ts | 1 + 8 files changed, 1332 insertions(+), 65 deletions(-) create mode 100644 .env.example create mode 100644 docs/FIREBASE_SETUP.md create mode 100644 src/firebase.ts create mode 100644 src/vite-env.d.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..23f7e25 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +VITE_FIREBASE_API_KEY= +VITE_FIREBASE_AUTH_DOMAIN= +VITE_FIREBASE_PROJECT_ID= +VITE_FIREBASE_STORAGE_BUCKET= +VITE_FIREBASE_MESSAGING_SENDER_ID= +VITE_FIREBASE_APP_ID= diff --git a/.github/workflows/pages-release.yml b/.github/workflows/pages-release.yml index 5056ce5..bff385d 100644 --- a/.github/workflows/pages-release.yml +++ b/.github/workflows/pages-release.yml @@ -18,6 +18,13 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + env: + VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }} + VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }} + VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }} + VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }} + VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }} + VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/docs/FIREBASE_SETUP.md b/docs/FIREBASE_SETUP.md new file mode 100644 index 0000000..8b94af6 --- /dev/null +++ b/docs/FIREBASE_SETUP.md @@ -0,0 +1,51 @@ +# Firebase Setup (GitHub Pages) + +## 1) Create Firebase project + +1. Open Firebase Console and create/select your project. +2. Add a **Web App**. +3. Copy the Firebase config values. + +## 2) Enable authentication + +1. Firebase Console → Authentication → Sign-in method. +2. Enable **Google** provider. +3. Add authorized domains: + - `localhost` + - `.github.io` + +## 3) Enable Firestore + +1. Firebase Console → Firestore Database → Create database. +2. Start in production mode. +3. Use these security rules: + +```txt +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /users/{userId}/contractions/{docId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + } +} +``` + +## 4) Local development config + +1. Copy `.env.example` to `.env`. +2. Paste your Firebase config values. +3. Run `npm run dev`. + +## 5) GitHub Pages secrets + +In GitHub repo settings, add Actions secrets: + +- `VITE_FIREBASE_API_KEY` +- `VITE_FIREBASE_AUTH_DOMAIN` +- `VITE_FIREBASE_PROJECT_ID` +- `VITE_FIREBASE_STORAGE_BUCKET` +- `VITE_FIREBASE_MESSAGING_SENDER_ID` +- `VITE_FIREBASE_APP_ID` + +The Pages workflow injects these at build time. diff --git a/package-lock.json b/package-lock.json index 149ca53..7cfbd2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "contraction-counter", "version": "0.1.0", "dependencies": { + "firebase": "^12.12.1", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -710,6 +711,648 @@ "node": ">=12" } }, + "node_modules/@firebase/ai": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.11.1.tgz", + "integrity": "sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.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.21", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.21.tgz", + "integrity": "sha512-j2y2q65BlgLGB5Pwjhv/Jopw2X/TBTzvAtI5z/DSp56U4wBj7LfhBfzbdCtFPges+Wz0g55GdoawXibOH5jGng==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.27.tgz", + "integrity": "sha512-ZObpYpAxL6JfgH7GnvlDD0sbzGZ0o4nijV8skatV9ZX49hJtCYbFqaEcPYptT94rgX1KUoKEderC7/fa7hybtw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.21", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.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.11", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.11.tgz", + "integrity": "sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.2.tgz", + "integrity": "sha512-jcXQVMHAQ5AEKzVD5C7s5fmAYeFOuN6lAJeNTgZK2B9aLnofWaJt8u1A8Idm8gpsBBYSaY3cVyeH5SWMOVPBLQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.2.tgz", + "integrity": "sha512-M91NhxqbSkI0ChkJWy69blC+rPr6HEgaeRllddSaU1pQ/7IiegeCQM9pPDIgvWnwnBSzKhUHpe6ro/jhJ+cvzw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.2", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.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.11", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.11.tgz", + "integrity": "sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.11", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.4.tgz", + "integrity": "sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.0" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.5.tgz", + "integrity": "sha512-IfVsafZ3QiXbsydXTP/XMI0wVYbJLI1rkb8Qqf03/h5FnL+upbbPOb+6Yj3RpcX+Y1iP5Uh18lxTHlXfbiyAow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.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.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.0.tgz", + "integrity": "sha512-mKkSLNym3UbnnZ06dAmtqzp5EpPGCANGCZDJbkoR135aoUdKG6Aizwcnp29RzsQpwH0nmy5nay17Sfbsh9oY8A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "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.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.2.tgz", + "integrity": "sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.6.0.tgz", + "integrity": "sha512-OiugPRcdlhqXF97oR9CjVObILmsWU0dFUS0gXNYEe4bDfpW8pZmQ5GqhIPPtLWbT/0W2lMJJD7VILFMk+xuHPg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.2.tgz", + "integrity": "sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.3.tgz", + "integrity": "sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/database": "1.1.2", + "@firebase/database-types": "1.0.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.19.tgz", + "integrity": "sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.4", + "@firebase/util": "1.15.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.14.0.tgz", + "integrity": "sha512-bZc6YOjRkMBVA16527tgzi6iN9n//xRB3Mmx/R+Gr6UAP/+xrIKOejQIcn1hh+tCzNT8jO0jI+kWox5J4tB/qQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@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.8", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.8.tgz", + "integrity": "sha512-WK9NJRpnosGD2nuyjdr7K+Ht7AxRYJlTF62myI4rRA7ibJOosbecvjacR5oirJ7s1BgNS6qzcBw7n4fD3a5w1w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/firestore": "4.14.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.15.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.3", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.3.tgz", + "integrity": "sha512-csO7ckK3SSs+NUZW1nms9EK7ckHe/1QOjiP8uAkCYa7ND18s44vjE9g3KxEeIUpyEPqZaX1EhJuFyZjHigAcYw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.2", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.3.tgz", + "integrity": "sha512-BxkEwWgx1of0tKaao/r2VR6WBLk/RAiyztatiONPrPE8gkitFkOnOCxf8i9cUyA5hX5RGt5H30uNn25Q6QNEmQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/functions": "0.13.3", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.15.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.21", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.21.tgz", + "integrity": "sha512-xGFGTeICJZ5vhrmmDukeczIcFULFXybojML2+QSDFoKj5A7zbGN7KzFGSKNhDkIxpjzsYG9IleJyUebuAcmqWA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.21.tgz", + "integrity": "sha512-zahIUkaVKbR8zmTeBHkdfaVl6JGWlhVoSjF7CVH33nFqD3SlPEpEEegn2GNT5iAfsVdtlCyJJ9GW4YKjq+RJKQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.15.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.25", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.25.tgz", + "integrity": "sha512-7RhDwoDHlOK1/ou0/LeubxmjcngsTjDdrY/ssg2vwAVpUuVAhQzQvuCAOYxcX5wNC1zCgQ54AP1vdngBwbCmOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.15.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.25.tgz", + "integrity": "sha512-eoOQqGLtRlseTdiemTN44LlHZpltK5gnhq8XVUuLgtIOG+odtDzrz2UoTpcJWSzaJQVxNLb/x9f39tHdDM4N4w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/messaging": "0.12.25", + "@firebase/util": "1.15.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.11", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.11.tgz", + "integrity": "sha512-V3uAhrz7IYJuji+OgT3qYTGKxpek/TViXti9OSsUJ4AexZ3jQjYH5Yrn7JvBxk8MGiSLsC872hh+BxQiPZsm7g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.24.tgz", + "integrity": "sha512-YRlejH8wLt7ThWao+HXoKUHUrZKGYq+otxkPS+8nuE5PeN1cBXX7NAJl9ueuUkBwMIrnKdnDqL/voHXxDAAt3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.11", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.15.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.8.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.2.tgz", + "integrity": "sha512-5EXqOThV4upjK9D38d/qOSVwOqRhemlaOFk9vCkMNNALeIlwr+4pLjtLNo4qoY8etQmU/1q4aIATE9N8PFqg0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/installations": "0.6.21", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.23.tgz", + "integrity": "sha512-4+KqRRHEUUmKT6tFmnpWATOsaFfmSuBs1jXH8JzVtMLEYqq/WS9IDM92OdefFDSrAA2xGd0WN004z8mKeIIscw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.8.2", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.2.tgz", + "integrity": "sha512-o/culaTeJ8GRpKXRJov21rux/n9dRaSOWLebyatFP2sqEdCxQPjVA1H9Z2fzYwQxMIU0JVmC7SPPmU11v7L6vQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.2.tgz", + "integrity": "sha512-R+aB38wxCH5zjIO/xu9KznI7fgiPuZAG98uVm1NcidHyyupGgIDLKigGmRGBZMnxibe/m2oxNKoZpfEbUX2aQQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/storage": "0.14.2", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.15.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.15.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.0.tgz", + "integrity": "sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "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/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -760,6 +1403,70 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1440,6 +2147,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1489,6 +2205,30 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/autoprefixer": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", @@ -1594,6 +2334,38 @@ ], "license": "CC-BY-4.0" }, + "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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1643,6 +2415,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/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -1700,12 +2478,83 @@ "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" } }, + "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/firebase": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.12.1.tgz", + "integrity": "sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.11.1", + "@firebase/analytics": "0.10.21", + "@firebase/analytics-compat": "0.2.27", + "@firebase/app": "0.14.11", + "@firebase/app-check": "0.11.2", + "@firebase/app-check-compat": "0.4.2", + "@firebase/app-compat": "0.5.11", + "@firebase/app-types": "0.9.4", + "@firebase/auth": "1.13.0", + "@firebase/auth-compat": "0.6.5", + "@firebase/data-connect": "0.6.0", + "@firebase/database": "1.1.2", + "@firebase/database-compat": "2.1.3", + "@firebase/firestore": "4.14.0", + "@firebase/firestore-compat": "0.4.8", + "@firebase/functions": "0.13.3", + "@firebase/functions-compat": "0.4.3", + "@firebase/installations": "0.6.21", + "@firebase/installations-compat": "0.2.21", + "@firebase/messaging": "0.12.25", + "@firebase/messaging-compat": "0.2.25", + "@firebase/performance": "0.7.11", + "@firebase/performance-compat": "0.2.24", + "@firebase/remote-config": "0.8.2", + "@firebase/remote-config-compat": "0.2.23", + "@firebase/storage": "0.14.2", + "@firebase/storage-compat": "0.4.2", + "@firebase/util": "1.15.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.0.tgz", + "integrity": "sha512-mKkSLNym3UbnnZ06dAmtqzp5EpPGCANGCZDJbkoR135aoUdKG6Aizwcnp29RzsQpwH0nmy5nay17Sfbsh9oY8A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.2", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.15.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -1745,6 +2594,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/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1752,6 +2610,27 @@ "dev": true, "license": "ISC" }, + "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/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/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2055,6 +2934,18 @@ "url": "https://opencollective.com/parcel" } }, + "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/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", @@ -2172,6 +3063,30 @@ "dev": true, "license": "MIT" }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "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/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2207,6 +3122,15 @@ "node": ">=0.10.0" } }, + "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/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -2252,6 +3176,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2281,6 +3225,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/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -2302,6 +3272,12 @@ "url": "https://opencollective.com/webpack" } }, + "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2316,6 +3292,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2407,12 +3389,94 @@ } } }, + "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/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" + } } } } diff --git a/package.json b/package.json index daf1331..137b5f3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "firebase": "^12.12.1", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/src/App.tsx b/src/App.tsx index 633208d..dda5b98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,28 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { Menu, Settings, Play, Square, Activity, CheckCircle } from 'lucide-react'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + Menu, + Play, + Square, + Activity, + CheckCircle, + LogIn, + LogOut, + Cloud, + CloudOff, + LoaderCircle, +} from 'lucide-react'; +import { User, onAuthStateChanged, signInWithPopup, signOut } from 'firebase/auth'; +import { collection, doc, onSnapshot, orderBy, query, setDoc, updateDoc } from 'firebase/firestore'; +import { auth, db, firebaseEnabled, googleProvider } from './firebase'; -// --- Types --- interface Contraction { id: string; startTime: number; endTime: number | null; } -// --- Helper Functions --- +const GUEST_STORAGE_KEY = 'contraction-counter-guest-history-v1'; + const formatMinSec = (ms: number): string => { if (ms < 0) return '0:00'; const totalSeconds = Math.floor(ms / 1000); @@ -22,27 +36,81 @@ const formatTimeOfDay = (timestamp: number): string => { return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; }; +const sortDesc = (list: Contraction[]) => [...list].sort((a, b) => b.startTime - a.startTime); + export default function App() { - // --- State --- const [contractions, setContractions] = useState([]); const [now, setNow] = useState(Date.now()); + const [user, setUser] = useState(null); + const [authLoading, setAuthLoading] = useState(true); + const [dataLoading, setDataLoading] = useState(false); + const [statusError, setStatusError] = useState(null); - // --- Effects --- useEffect(() => { let interval: ReturnType | undefined; - - // Keep the timer ticking as long as we have any contractions, - // so we can display the ongoing interval after hitting stop. if (contractions.length > 0) { interval = setInterval(() => setNow(Date.now()), 1000); } - return () => { if (interval) clearInterval(interval); }; }, [contractions]); - // --- Derived Stats --- + useEffect(() => { + if (!auth) { + setAuthLoading(false); + return; + } + const unsub = onAuthStateChanged(auth, (nextUser) => { + setUser(nextUser); + setAuthLoading(false); + }); + return () => unsub(); + }, []); + + useEffect(() => { + if (!db || !user) { + const raw = localStorage.getItem(GUEST_STORAGE_KEY); + if (raw) { + try { + const parsed = JSON.parse(raw) as Contraction[]; + setContractions(sortDesc(parsed)); + } catch { + setContractions([]); + } + } else { + setContractions([]); + } + return; + } + + setDataLoading(true); + const contractionQuery = query(collection(db, 'users', user.uid, 'contractions'), orderBy('startTime', 'desc')); + + const unsub = onSnapshot( + contractionQuery, + (snapshot) => { + const next = snapshot.docs.map((entry) => { + const value = entry.data() as { startTime: number; endTime: number | null }; + return { id: entry.id, startTime: value.startTime, endTime: value.endTime ?? null }; + }); + setContractions(next); + setDataLoading(false); + }, + () => { + setStatusError('Could not sync contractions from Firebase right now.'); + setDataLoading(false); + }, + ); + + return () => unsub(); + }, [user]); + + useEffect(() => { + if (user) return; + localStorage.setItem(GUEST_STORAGE_KEY, JSON.stringify(contractions)); + }, [contractions, user]); + const stats = useMemo(() => { let totalDuration = 0; let completedCount = 0; @@ -54,16 +122,13 @@ export default function App() { const sorted = [...contractions].sort((a, b) => a.startTime - b.startTime); sorted.forEach((c, index) => { - if (c.startTime >= oneHourAgo) { - pastHourCount++; - } + if (c.startTime >= oneHourAgo) pastHourCount++; if (c.endTime !== null) { totalDuration += c.endTime - c.startTime; completedCount++; } if (index > 0) { - const prev = sorted[index - 1]; - totalFrequency += c.startTime - prev.startTime; + totalFrequency += c.startTime - sorted[index - 1].startTime; frequencyCount++; } }); @@ -75,24 +140,19 @@ export default function App() { }; }, [contractions, now]); - // --- 4-1-1 Rule Logic --- const is411 = useMemo(() => { if (contractions.length < 2) return false; - // 1. Must have been tracking for at least an hour const oldest = [...contractions].sort((a, b) => a.startTime - b.startTime)[0]; if (now - oldest.startTime < 60 * 60 * 1000) return false; - // 2. Look at the data from the past hour const pastHourContractions = contractions.filter((c) => now - c.startTime <= 60 * 60 * 1000); - - // Ensure sufficient data points to establish a real pattern (e.g., at least 10 contractions) if (pastHourContractions.length < 10) return false; - let totalDur = 0, - completed = 0; - let totalFreq = 0, - freqCount = 0; + let totalDur = 0; + let completed = 0; + let totalFreq = 0; + let freqCount = 0; const sortedRecent = [...pastHourContractions].sort((a, b) => a.startTime - b.startTime); sortedRecent.forEach((c, i) => { @@ -109,58 +169,123 @@ export default function App() { const avgDur = completed > 0 ? totalDur / completed : 0; const avgFreq = freqCount > 0 ? totalFreq / freqCount : 0; - // 4-1-1 Criteria: Duration >= ~50s, Frequency <= ~5 mins (300,000ms), for 1 hour. - // (Giving generous boundaries to ensure it triggers in real human conditions) return avgDur >= 50000 && avgFreq <= 300000 && avgFreq > 0; }, [contractions, now]); - const activeContraction = contractions.find((c) => c.endTime === null); - const isTracking = !!activeContraction; + const displayContractions = sortDesc(contractions); + const activeContraction = displayContractions.find((c) => c.endTime === null); + const isTracking = Boolean(activeContraction); + + const handleToggle = async () => { + setStatusError(null); - // --- Handlers --- - const handleToggle = () => { if (isTracking && activeContraction) { - setContractions((prev) => - prev.map((c) => (c.id === activeContraction.id ? { ...c, endTime: Date.now() } : c)), - ); - } else { - const newContraction: Contraction = { - id: crypto.randomUUID(), - startTime: Date.now(), - endTime: null, - }; - setContractions((prev) => [newContraction, ...prev]); + const endedAt = Date.now(); + setContractions((prev) => prev.map((c) => (c.id === activeContraction.id ? { ...c, endTime: endedAt } : c))); + + if (db && user) { + try { + await updateDoc(doc(db, 'users', user.uid, 'contractions', activeContraction.id), { endTime: endedAt }); + } catch { + setStatusError('Failed to save contraction stop time. Please retry.'); + } + } + return; + } + + const newContraction: Contraction = { + id: crypto.randomUUID(), + startTime: Date.now(), + endTime: null, + }; + + setContractions((prev) => sortDesc([newContraction, ...prev])); + + if (db && user) { + try { + await setDoc(doc(db, 'users', user.uid, 'contractions', newContraction.id), newContraction); + } catch { + setStatusError('Failed to save contraction start time. Please retry.'); + } } }; - const displayContractions = [...contractions].sort((a, b) => b.startTime - a.startTime); + const handleGoogleSignIn = async () => { + if (!auth) return; + setStatusError(null); + try { + await signInWithPopup(auth, googleProvider); + } catch { + setStatusError('Google sign-in failed. Please try again.'); + } + }; + + const handleSignOut = async () => { + if (!auth) return; + setStatusError(null); + try { + await signOut(auth); + } catch { + setStatusError('Sign out failed. Please try again.'); + } + }; - // Calculate ongoing rest interval since the last contraction ended const lastContraction = displayContractions[0]; const timeSinceLastStop = - !isTracking && lastContraction && lastContraction.endTime !== null - ? now - lastContraction.endTime - : null; + !isTracking && lastContraction && lastContraction.endTime !== null ? now - lastContraction.endTime : null; return (
- {/* --- TOP APP BAR --- */}
-

Contractions

- + + {firebaseEnabled ? ( + user ? ( + + ) : ( + + ) + ) : ( + Firebase not configured + )}
- {/* --- SCROLLABLE CONTENT --- */}
- {/* Stats Grid */} +
+
+ {authLoading || dataLoading ? ( + <> + Syncing... + + ) : user ? ( + <> + Synced as {user.displayName ?? user.email} + + ) : ( + <> + Guest mode (local only) + + )} +
+ {statusError &&

{statusError}

} +
+
@@ -182,10 +307,8 @@ export default function App() {
- {/* Timeline Header */}

History

- {/* 4-1-1 Alert Banner */} {is411 && (
@@ -194,14 +317,13 @@ export default function App() {

4-1-1 Rule Reached!

- Contractions have been ~4 mins apart, lasting ~1 min, for over an hour. It's time to call the - doctor or head to the hospital! + Contractions have been ~4 mins apart, lasting ~1 min, for over an hour. It's time to call the doctor + or head to the hospital!

)} - {/* Timeline List */} {displayContractions.length > 0 ? (
{displayContractions.map((c, index) => { @@ -217,7 +339,6 @@ export default function App() { c.endTime === null ? 'border-rose-200 shadow-rose-100' : 'border-gray-100' } flex items-center justify-between transition-all z-10 relative`} > - {/* Left: Number indicator */}
- {/* Middle: Duration & Time */}
{formatMinSec(durationMs)} @@ -235,14 +355,12 @@ export default function App() {
Started {formatTimeOfDay(c.startTime)}
- {/* Right: Frequency */}
{frequencyStr}
Freq
- {/* Rest Time Connector (Between Contractions) */} {prevContraction && prevContraction.endTime !== null && (
@@ -269,9 +387,7 @@ export default function App() { )}
- {/* --- ACTION BUTTON SECTION --- */}
- {/* Ongoing Interval Indicator */} {timeSinceLastStop !== null && (
Resting Time diff --git a/src/firebase.ts b/src/firebase.ts new file mode 100644 index 0000000..677723b --- /dev/null +++ b/src/firebase.ts @@ -0,0 +1,21 @@ +import { initializeApp, getApp, getApps } from 'firebase/app'; +import { getAuth, GoogleAuthProvider } from 'firebase/auth'; +import { getFirestore } from 'firebase/firestore'; + +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string | undefined, + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string | undefined, + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID as string | undefined, + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET as string | undefined, + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID as string | undefined, + appId: import.meta.env.VITE_FIREBASE_APP_ID as string | undefined, +}; + +const hasFirebaseConfig = Object.values(firebaseConfig).every(Boolean); + +const app = hasFirebaseConfig ? (getApps().length ? getApp() : initializeApp(firebaseConfig)) : null; + +export const firebaseEnabled = Boolean(app); +export const auth = app ? getAuth(app) : null; +export const db = app ? getFirestore(app) : null; +export const googleProvider = new GoogleAuthProvider(); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// From a257487d487d7dc42fd7828e58a861ee34efe262 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 26 Apr 2026 15:12:25 -0600 Subject: [PATCH 2/4] feat(ui): responsive material-style redesign and pages/firebase setup improvements --- .env.example | 1 + .github/workflows/pages-release.yml | 4 + docs/FIREBASE_SETUP.md | 1 + src/App.tsx | 350 +++++++++++++++------------- src/index.css | 11 +- vite.config.ts | 4 +- 6 files changed, 205 insertions(+), 166 deletions(-) diff --git a/.env.example b/.env.example index 23f7e25..42a4306 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ VITE_FIREBASE_PROJECT_ID= VITE_FIREBASE_STORAGE_BUCKET= VITE_FIREBASE_MESSAGING_SENDER_ID= VITE_FIREBASE_APP_ID= +VITE_FIREBASE_MEASUREMENT_ID= diff --git a/.github/workflows/pages-release.yml b/.github/workflows/pages-release.yml index bff385d..6b27e56 100644 --- a/.github/workflows/pages-release.yml +++ b/.github/workflows/pages-release.yml @@ -1,6 +1,9 @@ name: Deploy to GitHub Pages on: + push: + branches: + - main workflow_dispatch: permissions: @@ -19,6 +22,7 @@ jobs: name: github-pages url: ${{ steps.deployment.outputs.page_url }} env: + GITHUB_PAGES: "true" VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }} VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }} VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }} diff --git a/docs/FIREBASE_SETUP.md b/docs/FIREBASE_SETUP.md index 8b94af6..2ff0ea7 100644 --- a/docs/FIREBASE_SETUP.md +++ b/docs/FIREBASE_SETUP.md @@ -47,5 +47,6 @@ In GitHub repo settings, add Actions secrets: - `VITE_FIREBASE_STORAGE_BUCKET` - `VITE_FIREBASE_MESSAGING_SENDER_ID` - `VITE_FIREBASE_APP_ID` +- `VITE_FIREBASE_MEASUREMENT_ID` (optional) The Pages workflow injects these at build time. diff --git a/src/App.tsx b/src/App.tsx index dda5b98..3bfc61a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -235,188 +235,212 @@ export default function App() { !isTracking && lastContraction && lastContraction.endTime !== null ? now - lastContraction.endTime : null; return ( -
-
- -

- - Contractions -

- - {firebaseEnabled ? ( - user ? ( - - ) : ( - - ) - ) : ( - Firebase not configured - )} -
+
+
+
+ -
-
-
- {authLoading || dataLoading ? ( - <> - Syncing... - - ) : user ? ( - <> - Synced as {user.displayName ?? user.email} - +

+ + Contraction Counter +

+ + {firebaseEnabled ? ( + user ? ( + ) : ( - <> - Guest mode (local only) - - )} -
- {statusError &&

{statusError}

} -
- -
-
- - {stats.avgDuration > 0 ? formatMinSec(stats.avgDuration) : '0:00'} - - Avg Dur -
- -
- {stats.pastHourCount} - Past Hour -
- -
- - {stats.avgFrequency > 0 ? formatMinSec(stats.avgFrequency) : '0:00'} + + ) + ) : ( + + Firebase not configured - Avg Freq -
+ )}
+
+ +
+
+
+
+
+ {authLoading || dataLoading ? ( + <> + Syncing... + + ) : user ? ( + <> + Synced as {user.displayName ?? user.email} + + ) : ( + <> + Guest mode (local only) + + )} +
+ {statusError &&

{statusError}

} +
-

History

+ {is411 && ( +
+
+ +
+
+

4-1-1 Rule Reached

+

+ Contractions are near 4 mins apart, lasting about 1 min, for over an hour. Time to call your + doctor or head to the hospital. +

+
+
+ )} - {is411 && ( -
-
- +
+

History

+ + {displayContractions.length > 0 ? ( +
+ {displayContractions.map((c, index) => { + const durationMs = c.endTime !== null ? c.endTime - c.startTime : now - c.startTime; + const prevContraction = displayContractions[index + 1]; + const frequencyStr = prevContraction ? formatMinSec(c.startTime - prevContraction.startTime) : '--'; + const nodeNumber = displayContractions.length - index; + + return ( + +
+
+ {nodeNumber} +
+ +
+
+ {formatMinSec(durationMs)} + {c.endTime === null && } +
+
Started {formatTimeOfDay(c.startTime)}
+
+ +
+
{frequencyStr}
+
Freq
+
+
+ + {prevContraction && prevContraction.endTime !== null && ( +
+
+
+ Rest: + + {formatMinSec(c.startTime - prevContraction.endTime)} + +
+
+ )} + + ); + })} +
+ ) : ( +
+
+ +
+

No contractions logged yet.

+

Tap the start button to begin tracking.

+
+ )}
-
-

4-1-1 Rule Reached!

-

- Contractions have been ~4 mins apart, lasting ~1 min, for over an hour. It's time to call the doctor - or head to the hospital! -

+
+ +
- )} - - {displayContractions.length > 0 ? ( -
- {displayContractions.map((c, index) => { - const durationMs = c.endTime !== null ? c.endTime - c.startTime : now - c.startTime; - const prevContraction = displayContractions[index + 1]; - const frequencyStr = prevContraction ? formatMinSec(c.startTime - prevContraction.startTime) : '--'; - const nodeNumber = displayContractions.length - index; - - return ( - -
-
- {nodeNumber} -
- -
-
- {formatMinSec(durationMs)} - {c.endTime === null && } -
-
Started {formatTimeOfDay(c.startTime)}
-
- -
-
{frequencyStr}
-
Freq
-
-
- {prevContraction && prevContraction.endTime !== null && ( -
-
-
- Rest: - - {formatMinSec(c.startTime - prevContraction.endTime)} - -
-
- )} -
- ); - })} -
- ) : ( -
-
- +
+ {timeSinceLastStop !== null && ( +
+ Resting Time +
+ {formatMinSec(timeSinceLastStop)} +
+
+ )} + +
-

No contractions logged yet.

-

Press the button below to start tracking.

-
- )} + +
-
- {timeSinceLastStop !== null && ( -
- Resting Time -
- {formatMinSec(timeSinceLastStop)} +
+
+ {timeSinceLastStop !== null && ( +
+ Resting Time +
{formatMinSec(timeSinceLastStop)}
-
- )} + )} -
diff --git a/src/index.css b/src/index.css index cc621a7..35a0b06 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,9 @@ @import 'tailwindcss'; +:root { + color-scheme: light; +} + html, body, #root { @@ -8,8 +12,11 @@ body, body { margin: 0; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; - background: #f8f9fa; + font-family: 'Google Sans', Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, + sans-serif; + background: #f6f8fc; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } /* Utility used by the app */ diff --git a/vite.config.ts b/vite.config.ts index 5797d20..5dca88d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +const isGitHubPages = process.env.GITHUB_PAGES === 'true'; + export default defineConfig({ plugins: [react()], - base: '/', + base: isGitHubPages ? '/ContractionCounter/' : '/', }); From af51fbeb0a4b41f7114edca0ef4f7ffd7484fe2c Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 26 Apr 2026 16:45:29 -0600 Subject: [PATCH 3/4] fix(auth): fallback to redirect for Google OAuth popup issues --- src/App.tsx | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3bfc61a..84c5e2f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,14 @@ import { CloudOff, LoaderCircle, } from 'lucide-react'; -import { User, onAuthStateChanged, signInWithPopup, signOut } from 'firebase/auth'; +import { + User, + onAuthStateChanged, + signInWithPopup, + signInWithRedirect, + signOut, +} from 'firebase/auth'; +import { FirebaseError } from 'firebase/app'; import { collection, doc, onSnapshot, orderBy, query, setDoc, updateDoc } from 'firebase/firestore'; import { auth, db, firebaseEnabled, googleProvider } from './firebase'; @@ -215,7 +222,24 @@ export default function App() { setStatusError(null); try { await signInWithPopup(auth, googleProvider); - } catch { + } catch (error) { + const code = error instanceof FirebaseError ? error.code : ''; + + if (code === 'auth/popup-blocked' || code === 'auth/operation-not-supported-in-this-environment') { + try { + await signInWithRedirect(auth, googleProvider); + return; + } catch { + setStatusError('Google sign-in needs a regular browser. Open this app in Chrome/Safari and try again.'); + return; + } + } + + if (code === 'auth/unauthorized-domain') { + setStatusError('Google sign-in domain is not authorized in Firebase yet. Add this site URL in Firebase Auth.'); + return; + } + setStatusError('Google sign-in failed. Please try again.'); } }; From a5fc519bc1b2b4ffc748286b196b9dbbe6000d14 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 26 Apr 2026 16:46:21 -0600 Subject: [PATCH 4/4] ci(pages): trigger deploy workflow on master and main --- .github/workflows/pages-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pages-release.yml b/.github/workflows/pages-release.yml index 6b27e56..692096c 100644 --- a/.github/workflows/pages-release.yml +++ b/.github/workflows/pages-release.yml @@ -3,6 +3,7 @@ name: Deploy to GitHub Pages on: push: branches: + - master - main workflow_dispatch: