commit ebc96b1f99c003113c1659349c1c1e078ba8f484 Author: kiran Date: Sat Feb 7 14:39:07 2026 +0530 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95662b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Node.js +node_modules/ +npm-debug.log +yarn-error.log +pnpm-debug.log* +dist/ +build/ +coverage/ +.npm/ + +# Editors +.vscode/ +.idea/ +*.swp +*.swo +*.swn +.project +.settings/ +.classpath + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS generated files +Thumbs.db +ehthumbs.db +Desktop.ini \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..79159ce --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Surge + +Surge is a monorepo project containing a mobile application and a backend service. + +## Project Structure + +- **mobile**: A React Native / Expo application. +- **backend**: A Node.js / TypeScript backend service. + +## Getting Started + +Please refer to the individual directories for specific setup and running instructions. \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..ce84f95 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1398 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/cors": "^11.2.0", + "dotenv": "^17.2.4", + "fastify": "^5.7.4", + "pg": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^25.2.1", + "@types/pg": "^8.16.0", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", + "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", + "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", + "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.7.4.tgz", + "integrity": "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.4.0.tgz", + "integrity": "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.0.tgz", + "integrity": "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..288b420 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,28 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node src/index.ts", + "dev": "nodemon src/index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@fastify/cors": "^11.2.0", + "dotenv": "^17.2.4", + "fastify": "^5.7.4", + "pg": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^25.2.1", + "@types/pg": "^8.16.0", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/backend/src/db/schema.sql b/backend/src/db/schema.sql new file mode 100644 index 0000000..bbcf857 --- /dev/null +++ b/backend/src/db/schema.sql @@ -0,0 +1,89 @@ +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + firebase_uid VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) NOT NULL, + display_name VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_users_firebase_uid ON users(firebase_uid); +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Challenges table +CREATE TABLE IF NOT EXISTS challenges ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, + description TEXT, + duration_days INTEGER NOT NULL, + difficulty VARCHAR(50) NOT NULL, -- 'beginner', 'moderate', 'hard', 'extreme' + is_active BOOLEAN DEFAULT TRUE, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_challenges_slug ON challenges(slug); +CREATE INDEX IF NOT EXISTS idx_challenges_is_active ON challenges(is_active); + +-- Challenge Requirements table +CREATE TABLE IF NOT EXISTS challenge_requirements ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + challenge_id UUID REFERENCES challenges(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + type VARCHAR(50) NOT NULL, -- 'BOOLEAN', 'NUMERIC', 'DURATION', 'PHOTO_PROOF' + validation_rules JSONB DEFAULT '{}', + sort_order INTEGER NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_challenge_requirements_challenge_id ON challenge_requirements(challenge_id); + +-- User Challenges table +CREATE TABLE IF NOT EXISTS user_challenges ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + challenge_id UUID REFERENCES challenges(id) ON DELETE CASCADE, + start_date DATE NOT NULL, + status VARCHAR(50) NOT NULL, -- 'ACTIVE', 'COMPLETED', 'FAILED', 'PAUSED' + current_streak INTEGER DEFAULT 0, + longest_streak INTEGER DEFAULT 0, + attempt_number INTEGER DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_challenges_user_status ON user_challenges(user_id, status); +CREATE INDEX IF NOT EXISTS idx_user_challenges_user_challenge ON user_challenges(user_id, challenge_id); + +-- Daily Progress table +CREATE TABLE IF NOT EXISTS daily_progress ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_challenge_id UUID REFERENCES user_challenges(id) ON DELETE CASCADE, + progress_date DATE NOT NULL, + day_number INTEGER NOT NULL, + is_complete BOOLEAN DEFAULT FALSE, + completed_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_challenge_id, progress_date) +); + +CREATE INDEX IF NOT EXISTS idx_daily_progress_user_challenge_date ON daily_progress(user_challenge_id, progress_date); + +-- Task Completions table +CREATE TABLE IF NOT EXISTS task_completions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + daily_progress_id UUID REFERENCES daily_progress(id) ON DELETE CASCADE, + requirement_id UUID REFERENCES challenge_requirements(id) ON DELETE CASCADE, + completion_data JSONB DEFAULT '{}', + completed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_task_completions_daily_progress ON task_completions(daily_progress_id); \ No newline at end of file diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts new file mode 100644 index 0000000..367507a --- /dev/null +++ b/backend/src/db/seed.ts @@ -0,0 +1,136 @@ +import { Pool } from 'pg'; +import * as fs from 'fs'; +import * as path from 'path'; + +const pool = new Pool({ + user: process.env.POSTGRES_USER || 'surge', + host: process.env.POSTGRES_HOST || 'localhost', + database: process.env.POSTGRES_DB || 'surge_db', + password: process.env.POSTGRES_PASSWORD || 'password', + port: parseInt(process.env.POSTGRES_PORT || '5432'), +}); + +const challenges = [ + { + name: '75 Hard', + slug: '75-hard', + description: 'The ultimate mental toughness challenge. No cheat meals, no alcohol, two workouts a day.', + duration_days: 75, + difficulty: 'extreme', + requirements: [ + { title: 'Two 45-minute workouts (one outdoors)', type: 'BOOLEAN', sort_order: 1 }, + { title: 'Follow a diet', type: 'BOOLEAN', sort_order: 2 }, + { title: 'No alcohol or cheat meals', type: 'BOOLEAN', sort_order: 3 }, + { title: 'Drink 1 gallon of water', type: 'BOOLEAN', sort_order: 4 }, + { title: 'Read 10 pages of non-fiction', type: 'BOOLEAN', sort_order: 5 }, + { title: 'Take a progress photo', type: 'PHOTO_PROOF', sort_order: 6 }, + ], + }, + { + name: '75 Soft', + slug: '75-soft', + description: 'A more sustainable version of 75 Hard for building consistent habits.', + duration_days: 75, + difficulty: 'moderate', + requirements: [ + { title: 'Eat well and only drink on social occasions', type: 'BOOLEAN', sort_order: 1 }, + { title: 'Train for 45 minutes everyday (one day of active recovery)', type: 'BOOLEAN', sort_order: 2 }, + { title: 'Drink 3 liters of water', type: 'BOOLEAN', sort_order: 3 }, + { title: 'Read 10 pages of any book', type: 'BOOLEAN', sort_order: 4 }, + ], + }, + { + name: '30-Day Fitness Challenge', + slug: '30-day-fitness', + description: 'A balanced fitness program to jumpstart your physical health.', + duration_days: 30, + difficulty: 'moderate', + requirements: [ + { title: '30 minutes of exercise', type: 'BOOLEAN', sort_order: 1 }, + { title: 'No sugar', type: 'BOOLEAN', sort_order: 2 }, + { title: 'Sleep 7+ hours', type: 'BOOLEAN', sort_order: 3 }, + ], + }, + { + name: '21-Day Meditation Challenge', + slug: '21-day-meditation', + description: 'Build a daily mindfulness practice in just three weeks.', + duration_days: 21, + difficulty: 'beginner', + requirements: [ + { title: 'Meditate for 10 minutes', type: 'BOOLEAN', sort_order: 1 }, + { title: 'Write in gratitude journal', type: 'BOOLEAN', sort_order: 2 }, + ], + }, + { + name: '30-Day No Sugar Challenge', + slug: '30-day-no-sugar', + description: 'Reset your palate and break the sugar addiction.', + duration_days: 30, + difficulty: 'moderate', + requirements: [ + { title: 'No added sugar', type: 'BOOLEAN', sort_order: 1 }, + { title: 'Check labels on all food', type: 'BOOLEAN', sort_order: 2 }, + { title: 'Eat 2 pieces of fruit max', type: 'BOOLEAN', sort_order: 3 }, + ], + }, +]; + +async function seed() { + const client = await pool.connect(); + try { + console.log('Starting database seed...'); + + // Read and execute schema + const schemaPath = path.join(__dirname, 'schema.sql'); + const schemaSql = fs.readFileSync(schemaPath, 'utf8'); + await client.query(schemaSql); + console.log('Schema applied successfully.'); + + // Clear existing challenges to avoid duplicates (optional, for development) + // await client.query('TRUNCATE challenges CASCADE'); + + for (const challenge of challenges) { + // Check if challenge exists + const res = await client.query('SELECT id FROM challenges WHERE slug = $1', [challenge.slug]); + + let challengeId; + if (res.rows.length > 0) { + console.log(`Challenge ${challenge.name} already exists, updating...`); + challengeId = res.rows[0].id; + // Update logic if needed, or skip + } else { + console.log(`Inserting challenge: ${challenge.name}`); + const insertRes = await client.query( + `INSERT INTO challenges (name, slug, description, duration_days, difficulty) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [challenge.name, challenge.slug, challenge.description, challenge.duration_days, challenge.difficulty] + ); + challengeId = insertRes.rows[0].id; + } + + // Insert requirements + // First delete existing requirements for this challenge to ensure clean state + await client.query('DELETE FROM challenge_requirements WHERE challenge_id = $1', [challengeId]); + + for (const req of challenge.requirements) { + await client.query( + `INSERT INTO challenge_requirements (challenge_id, title, type, sort_order) + VALUES ($1, $2, $3, $4)`, + [challengeId, req.title, req.type, req.sort_order] + ); + } + } + + console.log('Database seeding completed successfully.'); + } catch (err) { + console.error('Error seeding database:', err); + process.exit(1); + } finally { + client.release(); + await pool.end(); + } +} + +seed(); \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..074041b --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,92 @@ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const fastify = Fastify({ + logger: true +}); + +// Register CORS +fastify.register(cors, { + origin: true, // Allow all origins for development + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], +}); + +const pool = new Pool({ + user: process.env.POSTGRES_USER || 'surge', + host: process.env.POSTGRES_HOST || 'localhost', + database: process.env.POSTGRES_DB || 'surge_db', + password: process.env.POSTGRES_PASSWORD || 'password', + port: parseInt(process.env.POSTGRES_PORT || '5432'), +}); + +// Health check +fastify.get('/health', async (request, reply) => { + try { + await pool.query('SELECT 1'); + return { status: 'ok', db: 'connected' }; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ status: 'error', db: 'disconnected' }); + } +}); + +// GET /challenges +fastify.get('/challenges', async (request, reply) => { + try { + const result = await pool.query( + 'SELECT id, name, slug, description, duration_days, difficulty, is_active FROM challenges WHERE is_active = true ORDER BY name ASC' + ); + return result.rows; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: 'Internal Server Error' }); + } +}); + +// GET /challenges/:id +fastify.get<{ Params: { id: string } }>('/challenges/:id', async (request, reply) => { + const { id } = request.params; + try { + // Fetch challenge details + const challengeRes = await pool.query( + 'SELECT * FROM challenges WHERE id = $1', + [id] + ); + + if (challengeRes.rows.length === 0) { + return reply.code(404).send({ error: 'Challenge not found' }); + } + + const challenge = challengeRes.rows[0]; + + // Fetch requirements + const requirementsRes = await pool.query( + 'SELECT * FROM challenge_requirements WHERE challenge_id = $1 ORDER BY sort_order ASC', + [id] + ); + + return { + ...challenge, + requirements: requirementsRes.rows + }; + } catch (err) { + fastify.log.error(err); + return reply.code(500).send({ error: 'Internal Server Error' }); + } +}); + +const start = async () => { + try { + await fastify.listen({ port: 3000, host: '0.0.0.0' }); + console.log('Server listening on http://localhost:3000'); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +}; + +start(); \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..6c09fc5 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d3551f5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + ports: + - "5432:5432" + environment: + POSTGRES_USER: surge + POSTGRES_PASSWORD: password + POSTGRES_DB: surge_db + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + +volumes: + postgres_data: + redis_data: \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..48c3faf --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,457 @@ +# Architecture High-Level Design: Surge + +## Executive Summary + +This Architecture High-Level Design establishes the technical foundation for Surge, a mobile application enabling users to discover and complete structured self-improvement challenges. Building upon the Feature Definition's prioritization of the daily check-in experience and streak psychology, this architecture emphasizes responsive local-first interactions, reliable data synchronization, and a foundation that supports future social features without over-engineering the MVP. + +The design balances immediate delivery needs with strategic positioning for Phase 2 social capabilities, ensuring the core tracking experience remains fast and satisfying even under poor network conditions. + +*** + +## System Architecture Overview + +```mermaid +graph TB + subgraph "Client Layer" + MA[Mobile App
React Native] + LS[(Local Storage
SQLite/Realm)] + end + + subgraph "API Layer" + AG[API Gateway
AWS API Gateway] + AUTH[Auth Service
Firebase Auth] + end + + subgraph "Application Layer" + US[User Service] + CS[Challenge Service] + PS[Progress Service] + end + + subgraph "Data Layer" + PG[(PostgreSQL
Primary DB)] + RC[(Redis
Cache/Sessions)] + end + + subgraph "Supporting Services" + PN[Push Notifications
Firebase FCM] + AN[Analytics
Mixpanel/Amplitude] + end + + MA <--> LS + MA <--> AG + AG <--> AUTH + AG <--> US + AG <--> CS + AG <--> PS + US <--> PG + CS <--> PG + PS <--> PG + PS <--> RC + US <--> PN + MA --> AN +``` + +*** + +## Technology Stack + +### Mobile Application + +| Layer | Technology | Rationale | +| ----- | ---------- | --------- | +| Framework | React Native | Cross-platform efficiency, strong ecosystem, team familiarity | +| State Management | Zustand | Lightweight, minimal boilerplate, excellent for offline-first patterns | +| Local Database | WatermelonDB | Optimized for React Native, built-in sync capabilities, lazy loading | +| Navigation | React Navigation | Industry standard, deep linking support | +| UI Components | Custom + React Native Reanimated | Bold, high-energy design requires custom animations | + +### Backend Services + +| Component | Technology | Rationale | +| --------- | ---------- | --------- | +| Runtime | Node.js with TypeScript | Type safety, shared models with frontend, async performance | +| Framework | Fastify | High performance, schema validation, lower overhead than Express | +| Database | PostgreSQL 15 | ACID compliance, JSON support, proven reliability for user data | +| Cache | Redis | Session management, streak calculations, leaderboard preparation | +| Authentication | Firebase Auth | Rapid implementation, social login support, secure token management | + +### Infrastructure + +| Component | Technology | Rationale | +| --------- | ---------- | --------- | +| Cloud Provider | AWS | Comprehensive services, reliable, cost-effective at scale | +| Container Orchestration | AWS ECS Fargate | Serverless containers, reduced operational overhead | +| API Management | AWS API Gateway | Rate limiting, request validation, easy Lambda integration if needed | +| CDN | CloudFront | Challenge asset delivery, global edge caching | +| CI/CD | GitHub Actions | Integrated with codebase, cost-effective, extensive marketplace | + +*** + +## Core Component Design + +### Challenge Service + +Manages the challenge library and challenge definitions. As noted in Feature Definition, launching with 5 well-documented challenges is prioritized over quantity. + +```mermaid +classDiagram + class Challenge { + +uuid id + +string name + +string description + +int duration_days + +DailyRequirement[] requirements + +DifficultyLevel difficulty + +string[] tags + +boolean is_active + } + + class DailyRequirement { + +uuid id + +string title + +string description + +RequirementType type + +json validation_rules + +int sort_order + } + + class RequirementType { + <> + BOOLEAN + NUMERIC + DURATION + PHOTO_PROOF + } + + Challenge "1" --> "*" DailyRequirement + DailyRequirement --> RequirementType +``` + +**Design Decisions:** + +* Challenge definitions are admin-managed, cached aggressively on device +* Requirement types support future extensibility (photo proof for social features) +* Validation rules stored as JSON for flexible challenge-specific logic + +### Progress Service + +The heart of the user experience. Following Feature Definition's emphasis on making check-ins "fast, satisfying, and visually rewarding," this service prioritizes write performance and immediate feedback. + +```mermaid +classDiagram + class UserChallenge { + +uuid id + +uuid user_id + +uuid challenge_id + +date start_date + +ChallengeStatus status + +int current_streak + +int longest_streak + +int attempt_number + } + + class DailyProgress { + +uuid id + +uuid user_challenge_id + +date progress_date + +int day_number + +boolean is_complete + +timestamp completed_at + } + + class TaskCompletion { + +uuid id + +uuid daily_progress_id + +uuid requirement_id + +json completion_data + +timestamp completed_at + } + + class ChallengeStatus { + <> + ACTIVE + COMPLETED + FAILED + PAUSED + } + + UserChallenge "1" --> "*" DailyProgress + DailyProgress "1" --> "*" TaskCompletion + UserChallenge --> ChallengeStatus +``` + +**Streak Calculation Strategy:** + +* Current streak calculated on write (not read) for instant UI updates +* Redis maintains hot streak data for active users +* Nightly batch job reconciles any sync discrepancies +* `attempt_number` tracks restarts, supporting Feature Definition's "encouraging restart experience" + +### User Service + +Handles authentication, profile management, and notification preferences. + +```mermaid +sequenceDiagram + participant App + participant Firebase + participant API + participant DB + + App->>Firebase: Social Login (Google/Apple) + Firebase-->>App: ID Token + App->>API: POST /auth/verify + API->>Firebase: Verify Token + Firebase-->>API: User Claims + API->>DB: Upsert User + DB-->>API: User Record + API-->>App: JWT + User Profile + App->>App: Store JWT Securely +``` + +*** + +## Offline-First Architecture + +Given that daily check-ins are the core interaction, the app must function reliably regardless of network conditions. + +```mermaid +graph LR + subgraph "User Action" + A[Complete Task] + end + + subgraph "Local First" + B[Write to Local DB] + C[Update UI Immediately] + D[Queue Sync Operation] + end + + subgraph "Background Sync" + E{Network Available?} + F[Sync to Server] + G[Retry with Backoff] + H[Conflict Resolution] + end + + A --> B + B --> C + B --> D + D --> E + E -->|Yes| F + E -->|No| G + F --> H + G -.->|Retry| E +``` + +**Sync Strategy:** + +* All progress writes happen locally first, providing instant feedback +* Background sync with exponential backoff (5s, 15s, 45s, 2min max) +* Last-write-wins conflict resolution (acceptable for single-user MVP) +* Server timestamp used as source of truth for streak calculations +* Sync queue persisted to survive app termination + +*** + +## Data Architecture + +### PostgreSQL Schema (Simplified) + +```sql +-- Core tables with indexes optimized for common queries +users (id, firebase_uid, email, display_name, created_at, updated_at) + INDEX: firebase_uid (unique), email + +challenges (id, name, slug, duration_days, difficulty, is_active, metadata) + INDEX: slug (unique), is_active + +challenge_requirements (id, challenge_id, title, type, validation_rules, sort_order) + INDEX: challenge_id + +user_challenges (id, user_id, challenge_id, start_date, status, current_streak, attempt_number) + INDEX: (user_id, status), (user_id, challenge_id) + +daily_progress (id, user_challenge_id, progress_date, day_number, is_complete, completed_at) + INDEX: (user_challenge_id, progress_date) UNIQUE + +task_completions (id, daily_progress_id, requirement_id, completion_data, completed_at) + INDEX: daily_progress_id +``` + +### Redis Data Structures + +``` +# Active user streaks (hot data) +streak:{user_id}:{challenge_id} -> { current: 45, longest: 45, last_date: "2024-01-15" } +TTL: 7 days (refreshed on activity) + +# Session management +session:{token} -> { user_id, expires_at, device_id } +TTL: 30 days + +# Future: Leaderboard preparation +leaderboard:{challenge_id}:daily -> Sorted Set (user_id -> streak) +``` + +*** + +## API Design + +RESTful API with consistent patterns. Key endpoints: + +| Endpoint | Method | Purpose | +| -------- | ------ | ------- | +| `/challenges` | GET | List active challenges (cached) | +| `/challenges/{id}` | GET | Challenge details with requirements | +| `/me/challenges` | GET | User's active and past challenges | +| `/me/challenges` | POST | Start a new challenge | +| `/me/challenges/{id}/progress` | GET | Full progress for a challenge | +| `/me/challenges/{id}/today` | GET | Today's tasks and completion status | +| `/me/challenges/{id}/today` | PATCH | Update task completions | +| `/sync` | POST | Batch sync for offline changes | + +**Response Time Targets:** + +* Challenge library: <100ms (CDN cached) +* Today's progress: <150ms (Redis + DB) +* Task completion: <200ms (write path) + +*** + +## Security Architecture + +```mermaid +graph TB + subgraph "Client Security" + A[Secure Token Storage
iOS Keychain / Android Keystore] + B[Certificate Pinning] + C[Biometric Lock Option] + end + + subgraph "Transport Security" + D[TLS 1.3] + E[API Gateway Rate Limiting] + end + + subgraph "Backend Security" + F[JWT Validation] + G[Row-Level Security] + H[Input Validation
Fastify Schemas] + end + + A --> D + B --> D + D --> E + E --> F + F --> G + F --> H +``` + +**Key Security Measures:** + +* Firebase Auth handles credential security +* Short-lived JWTs (1 hour) with refresh token rotation +* All user data queries filtered by authenticated user\_id +* Rate limiting: 100 requests/minute per user +* Input validation at API gateway and service layers + +*** + +## Scalability Considerations + +**MVP Scale (10K users):** + +* Single PostgreSQL instance (db.t3.medium) +* Single Redis instance (cache.t3.micro) +* 2 ECS tasks behind ALB +* Estimated cost: \~$150/month + +**Growth Path (100K+ users):** + +* PostgreSQL read replicas for challenge library queries +* Redis cluster for streak calculations +* Horizontal scaling of stateless API services +* Consider Aurora Serverless for variable load + +**Social Features Preparation:** + +* User ID foreign keys in place for future friend relationships +* Redis sorted sets ready for leaderboard implementation +* Event-driven architecture allows adding notification triggers + +*** + +## Deployment Architecture + +```mermaid +graph TB + subgraph "Production" + ALB[Application Load Balancer] + ECS1[ECS Task 1] + ECS2[ECS Task 2] + RDS[(RDS PostgreSQL)] + REDIS[(ElastiCache Redis)] + end + + subgraph "CI/CD" + GH[GitHub Actions] + ECR[ECR Registry] + end + + subgraph "Monitoring" + CW[CloudWatch] + SENTRY[Sentry] + end + + GH --> ECR + ECR --> ECS1 + ECR --> ECS2 + ALB --> ECS1 + ALB --> ECS2 + ECS1 --> RDS + ECS2 --> RDS + ECS1 --> REDIS + ECS2 --> REDIS + ECS1 --> CW + ECS1 --> SENTRY +``` + +**Deployment Strategy:** + +* Blue/green deployments via ECS +* Database migrations run as pre-deployment task +* Feature flags for gradual rollouts +* Automated rollback on health check failures + +*** + +## Recommendations + +1. **Invest in Local-First Infrastructure**: The offline-first pattern is critical for the daily check-in experience. Allocate adequate time for sync logic and conflict handling. +2. **Implement Comprehensive Analytics Early**: As noted in Feature Definition, event tracking from day one informs Phase 2 social features. Instrument all user interactions. +3. **Design APIs for Mobile Efficiency**: Combine related data in single responses (today's tasks + streak + progress) to minimize round trips. +4. **Plan for Streak Edge Cases**: Timezone handling, daylight saving transitions, and missed-day scenarios need careful consideration in both client and server logic. +5. **Prepare Social Foundation Without Building It**: Include user\_id relationships and Redis structures that support leaderboards, but don't implement social features until validated. + +*** + +## Technical Risks & Mitigations + +| Risk | Impact | Mitigation | +| ---- | ------ | ---------- | +| Offline sync conflicts | Data loss, user frustration | Comprehensive conflict resolution, sync status UI | +| Streak calculation errors | Core feature broken | Server-side validation, reconciliation jobs, audit logs | +| Firebase Auth dependency | Authentication outage | Graceful degradation, cached sessions | +| React Native performance | Poor animation experience | Native driver animations, performance profiling | + +*** + +## Next Steps + +1. Set up infrastructure-as-code (Terraform/CDK) for reproducible environments +2. Implement authentication flow and user service +3. Build challenge service with seed data for 5 launch challenges +4. Develop progress service with offline-first client integration +5. Establish CI/CD pipeline with staging environment \ No newline at end of file diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 0000000..05e883e --- /dev/null +++ b/docs/features.md @@ -0,0 +1,317 @@ +# Feature Definition: Surge + +## Executive Summary + +Surge is a mobile application designed to help users discover, track, and complete structured self-improvement challenges. This feature definition establishes the core functionality required for MVP launch, focusing on the challenge library and personal progress tracking systems that form the foundation of the user experience. + +The MVP prioritizes delivering immediate value to fitness enthusiasts and habit builders by solving their primary pain point: staying organized and accountable during demanding multi-day challenges. Social features are intentionally deferred to post-MVP phases. + +*** + +## User Personas & Scenarios + +### Primary Personas + +**The Committed Challenger (Primary)** + +* Demographics: 25-40 years old, fitness-oriented +* Goals: Complete demanding challenges like 75 Hard without losing track +* Pain Points: Currently uses spreadsheets or notes apps, loses momentum mid-challenge +* Behavior: Checks progress daily, motivated by streaks and visual progress + +**The Habit Explorer (Secondary)** + +* Demographics: 20-35 years old, self-improvement focused +* Goals: Discover and try various challenge formats to build better habits +* Pain Points: Doesn't know which challenges exist or which suit their goals +* Behavior: Browses options before committing, prefers shorter challenges initially + +### Key User Scenarios + +**Scenario 1: Starting a New Challenge** +Sarah discovers Surge after deciding to attempt 75 Hard. She browses the challenge library, reads the specific requirements (two workouts, diet adherence, water intake, reading, progress photo), and starts the challenge with a clear understanding of daily expectations. + +**Scenario 2: Daily Check-in Routine** +Marcus is on Day 23 of his challenge. Each evening, he opens Surge, checks off completed tasks for the day, sees his streak count increase, and views his progress visualization showing he's 30% complete. + +**Scenario 3: Recovery from Missed Day** +Elena missed a requirement on Day 15 of 75 Hard (the challenge requires restarting on any failure). Surge clearly shows her the rule was broken, offers encouragement, and provides a simple path to restart with her history preserved. + +*** + +## Feature Specifications + +### Feature 1: Challenge Library + +**Purpose:** Provide users access to a curated collection of structured challenges with clear rules and requirements. + +**User Value:** Users no longer need to research challenge rules across multiple sources—everything is centralized and clearly explained. + +#### Functional Requirements + +| ID | Requirement | Priority | +| --- | ----------- | -------- | +| CL-01 | Display browsable list of available challenges | Must Have | +| CL-02 | Show challenge details: name, duration, description, difficulty | Must Have | +| CL-03 | List specific daily requirements for each challenge | Must Have | +| CL-04 | Filter challenges by duration and difficulty | Should Have | +| CL-05 | Search challenges by name or keyword | Should Have | +| CL-06 | Display challenge rules and failure conditions | Must Have | +| CL-07 | Show estimated daily time commitment | Nice to Have | + +#### MVP Challenge Content + +The library will launch with these challenges: + +1. **75 Hard** \- 75 days\, extreme difficulty +2. **75 Soft** \- 75 days\, moderate difficulty +3. **30-Day Fitness Challenge** \- 30 days\, moderate +4. **21-Day Meditation Challenge** \- 21 days\, beginner +5. **30-Day No Sugar Challenge** \- 30 days\, moderate + +```mermaid +graph TD + A[Challenge Library] --> B[Browse All] + A --> C[Filter/Search] + B --> D[Challenge Card] + C --> D + D --> E[Challenge Details] + E --> F[View Requirements] + E --> G[Start Challenge] + G --> H[Active Challenge] +``` + +#### Acceptance Criteria + +* User can view all available challenges within 2 seconds of opening library +* Each challenge displays duration, difficulty rating, and brief description on card +* Challenge detail view shows complete daily requirements list +* User can start any challenge with single tap from detail view + +*** + +### Feature 2: Personal Progress Tracking + +**Purpose:** Enable users to track daily completion of challenge requirements and visualize their journey. + +**User Value:** Replaces scattered tracking methods with a dedicated system that understands each challenge's specific structure. + +#### Functional Requirements + +| ID | Requirement | Priority | +| --- | ----------- | -------- | +| PT-01 | Display current day's required tasks as checkable items | Must Have | +| PT-02 | Save daily completion status persistently | Must Have | +| PT-03 | Show current streak count prominently | Must Have | +| PT-04 | Display overall progress percentage/visualization | Must Have | +| PT-05 | Track challenge start date and current day number | Must Have | +| PT-06 | Handle challenge-specific failure rules | Must Have | +| PT-07 | Allow viewing of past days' completion history | Should Have | +| PT-08 | Send daily reminder notifications | Should Have | +| PT-09 | Support multiple concurrent challenges | Nice to Have | + +#### Daily Check-in Flow + +```mermaid +sequenceDiagram + participant U as User + participant A as App + participant S as Storage + + U->>A: Open Daily View + A->>S: Fetch today's requirements + S-->>A: Return task list + status + A-->>U: Display checkable tasks + U->>A: Mark task complete + A->>S: Update completion status + A->>A: Recalculate streak/progress + A-->>U: Update UI with new stats + U->>A: Complete all tasks + A-->>U: Show celebration + streak update +``` + +#### Progress Visualization + +The progress dashboard displays: + +* **Current Day**: "Day 23 of 75" +* **Streak Counter**: Consecutive days completed +* **Progress Bar**: Visual percentage complete +* **Calendar View**: Month view with completion indicators +* **Today's Status**: Remaining tasks for current day + +#### Acceptance Criteria + +* Daily tasks reflect the specific challenge's requirements +* Checking off tasks updates immediately with visual feedback +* Streak count updates upon completing all daily requirements +* Progress percentage calculates accurately based on days completed +* Past completion data persists across app sessions +* Failed challenges (per challenge rules) prompt restart option + +*** + +### Feature 3: Challenge Lifecycle Management + +**Purpose:** Handle the complete journey from starting a challenge through completion or restart. + +**User Value:** Clear guidance through challenge states reduces confusion and maintains motivation. + +#### Challenge States + +```mermaid +stateDiagram-v2 + [*] --> NotStarted: User views challenge + NotStarted --> Active: Start Challenge + Active --> Active: Daily completion + Active --> Failed: Rule violation + Active --> Completed: All days finished + Failed --> Active: Restart + Completed --> [*]: Challenge archived + Failed --> NotStarted: Abandon +``` + +#### Functional Requirements + +| ID | Requirement | Priority | +| --- | ----------- | -------- | +| LM-01 | Track challenge state (not started, active, failed, completed) | Must Have | +| LM-02 | Display appropriate UI for each state | Must Have | +| LM-03 | Provide restart functionality with attempt history | Should Have | +| LM-04 | Show completion celebration on challenge finish | Should Have | +| LM-05 | Archive completed challenges with final stats | Should Have | + +*** + +### Feature 4: User Onboarding & Account + +**Purpose:** Get users into the app quickly while enabling data persistence. + +**User Value:** Minimal friction to start while ensuring progress is never lost. + +#### Functional Requirements + +| ID | Requirement | Priority | +| --- | ----------- | -------- | +| UA-01 | Email/password authentication | Must Have | +| UA-02 | Social login (Apple, Google) | Should Have | +| UA-03 | Brief onboarding highlighting key features | Should Have | +| UA-04 | Profile with basic settings | Must Have | +| UA-05 | Notification preferences management | Should Have | + +*** + +## MVP Scope Definition + +### In Scope (MVP) + +| Category | Included | +| -------- | -------- | +| Challenge Library | 5 curated challenges with complete requirements | +| Progress Tracking | Daily check-ins, streaks, progress visualization | +| Lifecycle | Start, track, complete, restart challenges | +| Account | Basic auth, profile, notification settings | +| Platform | iOS and Android via cross-platform framework | + +### Out of Scope (Post-MVP) + +| Feature | Phase | Rationale | +| ------- | ----- | --------- | +| Social/Friends | Phase 2 | Requires significant additional infrastructure | +| Leaderboards | Phase 2 | Depends on user base for meaningful competition | +| Custom Challenges | Phase 2 | Focus on curated quality first | +| Progress Photos | Phase 2 | Storage and privacy considerations | +| Community Forums | Phase 3 | Moderation requirements | +| Premium Subscriptions | Phase 2 | Establish value before monetization | + +*** + +## Success Metrics & KPIs + +### Primary Metrics + +| Metric | Target | Measurement | +| ------ | ------ | ----------- | +| Day 7 Retention | \>40% | Users returning 7 days after install | +| Challenge Start Rate | \>60% | Users who start a challenge within first session | +| Daily Check-in Rate | \>70% | Active challenge users checking in daily | +| Challenge Completion Rate | \>15% | Users completing their started challenge | + +### Secondary Metrics + +| Metric | Target | Measurement | +| ------ | ------ | ----------- | +| App Store Rating | \>4\.5 stars | Average user rating | +| Session Duration | \>2 min | Average time per session | +| Streak Length | \>7 days avg | Average streak before break | +| Restart Rate | <50% | Users restarting after failure (lower = better initial success) | + +### Tracking Implementation + +```mermaid +graph LR + A[User Actions] --> B[Analytics Events] + B --> C[Challenge Started] + B --> D[Daily Check-in] + B --> E[Streak Milestone] + B --> F[Challenge Completed] + B --> G[Challenge Failed/Restart] + C & D & E & F & G --> H[Dashboard] +``` + +*** + +## User Flow Summary + +```mermaid +graph TD + A[App Launch] --> B{Authenticated?} + B -->|No| C[Onboarding/Login] + B -->|Yes| D{Active Challenge?} + C --> D + D -->|No| E[Challenge Library] + D -->|Yes| F[Daily Dashboard] + E --> G[Challenge Details] + G --> H[Start Challenge] + H --> F + F --> I[Check Off Tasks] + I --> J{All Complete?} + J -->|Yes| K[Streak Updated] + J -->|No| L[Pending Tasks] + K --> M{Challenge Done?} + M -->|Yes| N[Completion Screen] + M -->|No| F +``` + +*** + +## Technical Considerations + +### Data Requirements + +* Challenge definitions (static, cacheable) +* User progress records (daily task completion) +* Streak calculations (derived from progress) +* User preferences and settings + +### Offline Capability + +* Challenge content available offline after initial load +* Daily check-ins queue when offline, sync when connected +* Progress data cached locally with cloud backup + +### Notification Strategy + +* Daily reminder at user-configured time +* Streak milestone celebrations (7, 14, 30, etc.) +* Gentle re-engagement after 2 days of inactivity + +*** + +## Recommendations + +1. **Prioritize Daily Experience**: The check-in flow is the core interaction—invest heavily in making it fast, satisfying, and visually rewarding. +2. **Nail the Streak Psychology**: Streaks are the primary motivation mechanism. Consider streak freezes or recovery options for user retention. +3. **Quality Over Quantity**: Launch with 5 well-documented challenges rather than 20 incomplete ones. Each challenge should feel professionally curated. +4. **Design for Failure Recovery**: Most users will fail demanding challenges. The restart experience should feel encouraging, not punishing. +5. **Build Analytics Foundation**: Implement comprehensive event tracking from day one to inform Phase 2 social features. \ No newline at end of file diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..f8c6c2e --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,43 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +# generated native folders +/ios +/android diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..48dd63f --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app 👋 + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/mobile/app.json b/mobile/app.json new file mode 100644 index 0000000..80f0da4 --- /dev/null +++ b/mobile/app.json @@ -0,0 +1,48 @@ +{ + "expo": { + "name": "mobile", + "slug": "mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "mobile", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/images/android-icon-foreground.png", + "backgroundImage": "./assets/images/android-icon-background.png", + "monochromeImage": "./assets/images/android-icon-monochrome.png" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false + }, + "web": { + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff", + "dark": { + "backgroundColor": "#000000" + } + } + ] + ], + "experiments": { + "typedRoutes": true, + "reactCompiler": true + } + } +} diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..54e11d0 --- /dev/null +++ b/mobile/app/(tabs)/_layout.tsx @@ -0,0 +1,35 @@ +import { Tabs } from 'expo-router'; +import React from 'react'; + +import { HapticTab } from '@/components/haptic-tab'; +import { IconSymbol } from '@/components/ui/icon-symbol'; +import { Colors } from '@/constants/theme'; +import { useColorScheme } from '@/hooks/use-color-scheme'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/mobile/app/(tabs)/explore.tsx b/mobile/app/(tabs)/explore.tsx new file mode 100644 index 0000000..3943752 --- /dev/null +++ b/mobile/app/(tabs)/explore.tsx @@ -0,0 +1,165 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, FlatList, TouchableOpacity, RefreshControl } from 'react-native'; +import { useRouter } from 'expo-router'; +import { withObservables } from '@nozbe/watermelondb/react'; +import { database } from '../../src/db'; +import Challenge from '../../src/db/models/Challenge'; +import { syncChallenges } from '../../src/db/sync'; +import { Colors } from '@/constants/theme'; + +const ChallengeItem = ({ challenge, onPress }: { challenge: Challenge; onPress: () => void }) => ( + + + {challenge.name} + + {challenge.difficulty} + + + + {challenge.description} + + + {challenge.durationDays} Days + + +); + +const getDifficultyColor = (difficulty: string) => { + switch (difficulty?.toLowerCase()) { + case 'beginner': return '#4CAF50'; + case 'moderate': return '#FF9800'; + case 'hard': return '#F44336'; + case 'extreme': return '#9C27B0'; + default: return '#757575'; + } +}; + +const ChallengeList = ({ challenges }: { challenges: Challenge[] }) => { + const router = useRouter(); + const [refreshing, setRefreshing] = useState(false); + + const onRefresh = async () => { + setRefreshing(true); + await syncChallenges(); + setRefreshing(false); + }; + + useEffect(() => { + // Initial sync + syncChallenges(); + }, []); + + return ( + + Explore Challenges + item.id} + renderItem={({ item }) => ( + router.push(`/challenge/${item.id}`)} + /> + )} + contentContainerStyle={styles.listContent} + refreshControl={ + + } + ListEmptyComponent={ + + No challenges found. + Pull to refresh to load challenges. + + } + /> + + ); +}; + +const enhance = withObservables([], () => ({ + challenges: database.get('challenges').query(), +})); + +export default enhance(ChallengeList); + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + paddingTop: 60, // Status bar spacing + }, + title: { + fontSize: 28, + fontWeight: 'bold', + paddingHorizontal: 20, + marginBottom: 16, + color: '#333', + }, + listContent: { + paddingHorizontal: 20, + paddingBottom: 20, + }, + card: { + backgroundColor: 'white', + borderRadius: 12, + padding: 16, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8, + }, + cardTitle: { + fontSize: 18, + fontWeight: '600', + color: '#333', + flex: 1, + marginRight: 8, + }, + badge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + }, + badgeText: { + color: 'white', + fontSize: 12, + fontWeight: 'bold', + textTransform: 'capitalize', + }, + cardDescription: { + fontSize: 14, + color: '#666', + marginBottom: 12, + lineHeight: 20, + }, + cardFooter: { + flexDirection: 'row', + alignItems: 'center', + }, + durationText: { + fontSize: 14, + color: '#888', + fontWeight: '500', + }, + emptyContainer: { + padding: 40, + alignItems: 'center', + }, + emptyText: { + fontSize: 18, + color: '#666', + marginBottom: 8, + }, + emptySubText: { + fontSize: 14, + color: '#999', + }, +}); diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx new file mode 100644 index 0000000..ab25897 --- /dev/null +++ b/mobile/app/(tabs)/index.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; +import { useRouter } from 'expo-router'; +import { withObservables } from '@nozbe/watermelondb/react'; +import { database } from '../../src/db'; +import UserChallenge from '../../src/db/models/UserChallenge'; +import { Q } from '@nozbe/watermelondb'; +import ProgressScreen from '../screens/ProgressScreen'; +import { Colors } from '@/constants/theme'; + +const HomeScreen = ({ userChallenge }: { userChallenge: UserChallenge | null }) => { + const router = useRouter(); + + const handleReset = async () => { + Alert.alert( + "Reset Data", + "Are you sure you want to clear all local data? This cannot be undone.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Reset", + style: "destructive", + onPress: async () => { + try { + await database.write(async () => { + await database.unsafeResetDatabase(); + }); + // Reload or just let the UI update (observables should handle it, but reset might need reload) + // For now, unsafeResetDatabase might require app reload in some contexts, + // but let's see if observables pick up the empty state. + // Actually, unsafeResetDatabase clears everything. + } catch (e) { + console.error("Error resetting database:", e); + } + } + } + ] + ); + }; + + if (userChallenge) { + return ( + + + + Debug: Reset Data + + + ); + } + + return ( + + Welcome to Surge + You don{`'`}t have an active challenge yet. + + router.push('/(tabs)/explore')} + > + Explore Challenges + + + + Debug: Reset Data + + + ); +}; + +const enhance = withObservables([], () => ({ + userChallenge: database.get('user_challenges') + .query(Q.where('status', 'active')) + .observeWithColumns(['status']) + .pipe( + // @ts-ignore + require('rxjs/operators').map((challenges: UserChallenge[]) => challenges.length > 0 ? challenges[0] : null) + ), +})); + +export default enhance(HomeScreen); + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + backgroundColor: '#f5f5f5', + }, + title: { + fontSize: 32, + fontWeight: 'bold', + marginBottom: 10, + color: '#333', + }, + subtitle: { + fontSize: 18, + color: '#666', + marginBottom: 40, + textAlign: 'center', + }, + button: { + backgroundColor: Colors.light.tint, + paddingHorizontal: 30, + paddingVertical: 15, + borderRadius: 25, + marginBottom: 20, + }, + buttonText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + }, + debugButton: { + marginTop: 20, + padding: 10, + }, + debugButtonText: { + color: 'red', + fontSize: 14, + } +}); diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx new file mode 100644 index 0000000..f518c9b --- /dev/null +++ b/mobile/app/_layout.tsx @@ -0,0 +1,24 @@ +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import 'react-native-reanimated'; + +import { useColorScheme } from '@/hooks/use-color-scheme'; + +export const unstable_settings = { + anchor: '(tabs)', +}; + +export default function RootLayout() { + const colorScheme = useColorScheme(); + + return ( + + + + + + + + ); +} diff --git a/mobile/app/challenge/[id].tsx b/mobile/app/challenge/[id].tsx new file mode 100644 index 0000000..09befe5 --- /dev/null +++ b/mobile/app/challenge/[id].tsx @@ -0,0 +1,272 @@ +import React, { useEffect, useState } from 'react'; +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { withObservables } from '@nozbe/watermelondb/react'; +import { database } from '../../src/db'; +import Challenge from '../../src/db/models/Challenge'; +import ChallengeRequirement from '../../src/db/models/ChallengeRequirement'; +import { syncChallengeDetails } from '../../src/db/sync'; +import UserChallenge from '../../src/db/models/UserChallenge'; +import User from '../../src/db/models/User'; + +const ChallengeDetailScreen = ({ challenge, requirements }: { challenge: Challenge; requirements: ChallengeRequirement[] }) => { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [starting, setStarting] = useState(false); + + useEffect(() => { + if (challenge) { + setLoading(true); + syncChallengeDetails(challenge.id).finally(() => setLoading(false)); + } + }, [challenge]); + + const handleStartChallenge = async () => { + if (!challenge) return; + + setStarting(true); + try { + await database.write(async () => { + const userChallengesCollection = database.get('user_challenges'); + + // Check if already active + // Note: In a real app, we'd check for existing active challenges for this user + // For now, we'll just create a new one. + // We need a user ID. Since we don't have auth fully set up in this context, + // we'll use a placeholder or fetch the first user if available. + + const usersCollection = database.get('users'); + const users = await usersCollection.query().fetch(); + let userId = 'temp-user-id'; + if (users.length > 0) { + userId = users[0].id; + } else { + // Create a temp user if none exists (for testing) + const newUser = await usersCollection.create(user => { + user.firebaseUid = 'temp-uid'; + user.email = 'test@example.com'; + }); + userId = newUser.id; + } + + await userChallengesCollection.create(uc => { + uc.user.id = userId; + uc.challenge.set(challenge); + uc.startDate = new Date().getTime(); + uc.status = 'active'; + uc.currentStreak = 0; + uc.longestStreak = 0; + uc.attemptNumber = 1; + uc.createdAt = new Date().getTime(); + }); + }); + + Alert.alert('Success', 'Challenge started!', [ + { text: 'OK', onPress: () => router.replace('/') } + ]); + } catch (error) { + console.error('Error starting challenge:', error); + Alert.alert('Error', 'Failed to start challenge.'); + } finally { + setStarting(false); + } + }; + + if (!challenge) { + return ( + + Loading challenge... + + ); + } + + return ( + + + {challenge.name} + + + {challenge.difficulty} + + {challenge.durationDays} Days + + + + {challenge.description} + + + Daily Requirements + {loading && requirements.length === 0 ? ( + + ) : ( + requirements.map((req, index) => ( + + {index + 1} + + {req.title} + {req.description ? {req.description} : null} + + + )) + )} + + + + {starting ? ( + + ) : ( + Start Challenge + )} + + + ); +}; + +const enhance = withObservables(['id'], ({ id }: { id: string }) => ({ + challenge: database.get('challenges').findAndObserve(id), + requirements: database.get('challenge_requirements') + .query( + // We can't easily query by relation in withObservables without Q.on + // But since we are syncing details which updates requirements, + // we can just query by challenge_id if we had the challenge object available immediately. + // However, 'challenge' prop is async. + // A common pattern is to pass the challenge itself or query requirements based on the ID prop. + ).observeWithColumns(['title', 'description', 'sort_order']) + // Wait, the above query is empty. We need to filter by challenge_id. + // But we only have `id` (challenge id) from props. +})); + +// Correct way to query related records with WatermelonDB observables +const enhanceWithRelated = withObservables(['id'], ({ id }: { id: string }) => { + const challengeObservable = database.get('challenges').findAndObserve(id); + + // We need to return an object where keys are prop names and values are observables + return { + challenge: challengeObservable, + // To get requirements, we can't use `challenge.requirements` directly here because `challenge` is an observable, not the record yet. + // But we can query the requirements table directly using the ID. + requirements: database.get('challenge_requirements') + .query( + // @ts-ignore + // We need to import Q from watermelondb + // But for now let's assume the query is correct + require('@nozbe/watermelondb').Q.where('challenge_id', id), + require('@nozbe/watermelondb').Q.sortBy('sort_order', require('@nozbe/watermelondb').Q.asc) + ) + }; +}); + +export default function ChallengeDetailWrapper() { + const { id } = useLocalSearchParams(); + const ChallengeDetail = enhanceWithRelated(ChallengeDetailScreen); + + // Ensure ID is a string + const challengeId = Array.isArray(id) ? id[0] : id; + + if (!challengeId) return null; + + return ; +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + padding: 20, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + header: { + marginBottom: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#333', + marginBottom: 10, + }, + metaContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + badge: { + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: 15, + marginRight: 10, + }, + badgeText: { + fontSize: 12, + fontWeight: 'bold', + color: '#555', + textTransform: 'uppercase', + }, + duration: { + fontSize: 14, + color: '#666', + }, + description: { + fontSize: 16, + color: '#444', + lineHeight: 24, + marginBottom: 30, + }, + section: { + marginBottom: 30, + }, + sectionTitle: { + fontSize: 20, + fontWeight: '600', + marginBottom: 15, + color: '#333', + }, + requirementItem: { + flexDirection: 'row', + marginBottom: 15, + backgroundColor: '#f9f9f9', + padding: 15, + borderRadius: 10, + }, + reqIndex: { + fontSize: 16, + fontWeight: 'bold', + color: '#007AFF', + marginRight: 15, + width: 24, + }, + reqContent: { + flex: 1, + }, + reqTitle: { + fontSize: 16, + fontWeight: '500', + color: '#333', + marginBottom: 4, + }, + reqDesc: { + fontSize: 14, + color: '#666', + }, + startButton: { + backgroundColor: '#007AFF', + paddingVertical: 16, + borderRadius: 12, + alignItems: 'center', + marginBottom: 40, + }, + disabledButton: { + opacity: 0.7, + }, + startButtonText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + }, +}); \ No newline at end of file diff --git a/mobile/app/modal.tsx b/mobile/app/modal.tsx new file mode 100644 index 0000000..6dfbc1a --- /dev/null +++ b/mobile/app/modal.tsx @@ -0,0 +1,29 @@ +import { Link } from 'expo-router'; +import { StyleSheet } from 'react-native'; + +import { ThemedText } from '@/components/themed-text'; +import { ThemedView } from '@/components/themed-view'; + +export default function ModalScreen() { + return ( + + This is a modal + + Go to home screen + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/mobile/app/progress.tsx b/mobile/app/progress.tsx new file mode 100644 index 0000000..09e5952 --- /dev/null +++ b/mobile/app/progress.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import ProgressScreen from './screens/ProgressScreen' + +export default function ProgressRoute() { + return +} \ No newline at end of file diff --git a/mobile/app/screens/ChallengeDetailScreen.tsx b/mobile/app/screens/ChallengeDetailScreen.tsx new file mode 100644 index 0000000..d2f93a1 --- /dev/null +++ b/mobile/app/screens/ChallengeDetailScreen.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { View, Text, StyleSheet, Button } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; + +export default function ChallengeDetailScreen() { + const navigation = useNavigation(); + + return ( + + Challenge Details +