diff --git a/.gitignore b/.gitignore index f390d12..61bd6c3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ yarn-error.log* next-env.d.ts /src/generated/prisma +.gstack/ diff --git a/bun.lock b/bun.lock index c090786..a556449 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,12 @@ "": { "name": "inner-os", "dependencies": { - "@clerk/nextjs": "^7.3.7", + "@prisma/adapter-better-sqlite3": "^7.8.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.10.0", + "jose": "^6.2.3", "next": "16.2.6", "openai": "^6.38.0", "react": "19.2.4", @@ -15,6 +18,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^3.0.0", "@types/node": "^20.19.41", "@types/react": "^19", "@types/react-dom": "^19", @@ -62,14 +66,6 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, ""], - "@clerk/backend": ["@clerk/backend@3.4.11", "", { "dependencies": { "@clerk/shared": "^4.12.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-WT+4FrcMMofxe+irQYUkawoRWaHHXT9/wiRYhRbSb30cOPjN95ViydqPhAyp8ZWAHInHg4mILynQQRJH14SKZQ=="], - - "@clerk/nextjs": ["@clerk/nextjs@7.3.7", "", { "dependencies": { "@clerk/backend": "^3.4.11", "@clerk/react": "^6.6.6", "@clerk/shared": "^4.12.2", "server-only": "0.0.1", "tslib": "2.8.1" }, "peerDependencies": { "next": "^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0", "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-CLG63zKPHditk52Z/+c6BJ47V1Q68ACjeps0xHDRW3X/Yv/FZdM+IUKu9Egb5PJ/TkRFxsI1nU5SOY2B8o/WxA=="], - - "@clerk/react": ["@clerk/react@6.6.6", "", { "dependencies": { "@clerk/shared": "^4.12.2", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-MVHLDZeGobSbGSZgAdb4G1BbB8ZU5XAmBBdIVLXiPOLEst2TYM5bP137WRA76y9GlmVmKYdKzph/TUi87P/JOA=="], - - "@clerk/shared": ["@clerk/shared@4.12.2", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-jDkip8tKTzYz/cPKMCsjOoACH3Xh37zcbCrssMRTYOq3GZypIpZ6WAs4m4G82URL0WY+yz5frrHVjRrHyAb6LA=="], - "@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="], "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="], @@ -152,6 +148,8 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, ""], + "@prisma/adapter-better-sqlite3": ["@prisma/adapter-better-sqlite3@7.8.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.8.0", "better-sqlite3": "^12.6.0" } }, "sha512-9p0HvQNaRoOaUForDcp1MPIjylmCamYQjQpEogpdesLr14g3NwAsYR3bHL9wFi8tn21TPDVQPzcZi+SxXEokSA=="], + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.8.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.8.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg=="], "@prisma/client": ["@prisma/client@7.8.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.8.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" } }, "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw=="], @@ -198,8 +196,6 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, ""], - "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, ""], @@ -212,7 +208,7 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, ""], - "@tanstack/query-core": ["@tanstack/query-core@5.100.11", "", {}, "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw=="], + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], "@types/estree": ["@types/estree@1.0.9", "", {}, ""], @@ -292,16 +288,28 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, ""], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": "dist/cli.cjs" }, ""], + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + "better-result": ["better-result@2.9.2", "", {}, "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q=="], + "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""], "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, ""], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, ""], @@ -320,6 +328,8 @@ "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "client-only": ["client-only@0.0.1", "", {}, ""], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], @@ -346,6 +356,10 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "", {}, ""], "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], @@ -358,8 +372,6 @@ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, ""], @@ -378,6 +390,8 @@ "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.21.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, ""], "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], @@ -434,6 +448,8 @@ "esutils": ["esutils@2.0.3", "", {}, ""], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], @@ -446,8 +462,6 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, ""], - "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, ""], @@ -456,6 +470,8 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, ""], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""], @@ -468,6 +484,8 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "function-bind": ["function-bind@1.1.2", "", {}, ""], "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, ""], @@ -492,9 +510,9 @@ "giget": ["giget@3.2.0", "", { "bin": "dist/cli.mjs" }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""], "globals": ["globals@16.4.0", "", {}, ""], @@ -532,12 +550,18 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, ""], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], "imurmurhash": ["imurmurhash@0.1.4", "", {}, ""], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, ""], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, ""], @@ -602,7 +626,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, ""], - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, ""], @@ -652,10 +676,14 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""], "minimist": ["minimist@1.2.8", "", {}, ""], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, ""], "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], @@ -664,12 +692,16 @@ "nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, ""], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": "lib/cli.js" }, ""], "natural-compare": ["natural-compare@1.4.0", "", {}, ""], "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, ""], + "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, ""], "node-releases": ["node-releases@2.0.44", "", {}, ""], @@ -692,6 +724,8 @@ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws"], "bin": "bin/cli" }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, ""], @@ -750,6 +784,8 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, ""], "prisma": ["prisma@7.8.0", "", { "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3"], "bin": "build/index.js" }, "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw=="], @@ -758,12 +794,16 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "punycode": ["punycode@2.3.1", "", {}, ""], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, ""], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], "react": ["react@19.2.4", "", {}, ""], @@ -772,6 +812,8 @@ "react-is": ["react-is@16.13.1", "", {}, ""], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, ""], @@ -796,6 +838,8 @@ "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, ""], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, ""], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, ""], @@ -808,8 +852,6 @@ "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], - "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="], - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, ""], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, ""], @@ -832,6 +874,10 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, ""], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -840,8 +886,6 @@ "stable-hash": ["stable-hash@0.0.5", "", {}, ""], - "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, ""], @@ -858,6 +902,8 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, ""], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""], @@ -872,6 +918,10 @@ "tapable": ["tapable@2.3.3", "", {}, ""], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, ""], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""], @@ -882,6 +932,8 @@ "tslib": ["tslib@2.8.1", "", {}, ""], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, ""], @@ -906,6 +958,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" } }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], @@ -920,6 +974,8 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, ""], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yallist": ["yallist@3.1.1", "", {}, ""], @@ -968,10 +1024,14 @@ "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, ""], + "node-abi/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, ""], + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "sharp/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, ""], "@prisma/streams-local/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/dictionaries/en.ts b/dictionaries/en.ts new file mode 100644 index 0000000..fd731a4 --- /dev/null +++ b/dictionaries/en.ts @@ -0,0 +1,266 @@ +import type { Dict } from "@/lib/i18n/types"; + +const en: Dict = { + // ----------------------------------------------------------------------- + // Navigation (TabBar) + // ----------------------------------------------------------------------- + nav: { + today: "Today", + progress: "Progress", + rewards: "Rewards", + summary: "Summary", + settings: "Settings", + }, + + // ----------------------------------------------------------------------- + // Shared labels + // ----------------------------------------------------------------------- + shared: { + loading: "Loading...", + back: "← Back", + + task_type: { + attention: "Attention", + emotion: "Emotion", + stress: "Stress", + clarity: "Clarity", + integration: "Integration", + }, + + difficulty: { + beginner: "Beginner", + intermediate: "Intermediate", + advanced: "Advanced", + }, + + stage: { + beginner: "Beginner", + foundation: "Foundation", + growing: "Growing", + established: "Established", + advanced: "Advanced", + }, + + reward_type: { + daily_completion: "Daily Practice", + streak_3: "3-Day Streak", + streak_7: "7-Day Streak", + streak_14: "14-Day Streak", + streak_30: "30-Day Streak", + stage_up: "Stage Advance", + first_session: "First Practice", + }, + }, + + // ----------------------------------------------------------------------- + // Home page + // ----------------------------------------------------------------------- + home: { + greeting: { + morning: "Good morning", + afternoon: "Good afternoon", + evening: "Good evening", + fallback_name: "there", + subtitle: "Your practice is waiting.", + }, + + onboarding: { + title: "One step left", + description: "Complete onboarding in Settings to start your practice.", + cta: "Go to Settings", + }, + + stats: { + streak: "Streak", + streak_unit: "days", + sessions: "Sessions", + sessions_unit: "total", + }, + + metrics: { + title: "Core metrics", + attention: "Attention", + emotion: "Emotion", + stress: "Stress", + consistency: "Consistency", + }, + + task: { + heading: "Today's practice", + no_task: "No task for today yet. Generate one to start your practice.", + generate: "Generate today's task", + generating: "Generating...", + begin: "Begin practice", + why_prefix: "", // unused in English, kept for key parity with zh + }, + }, + + // ----------------------------------------------------------------------- + // Progress page + // ----------------------------------------------------------------------- + progress: { + title: "Your progress", + metrics_title: "Core metrics", + stage_title: "Stage progress", + + attention: "Attention", + emotional_stability: "Emotional Stability", + stress_level: "Stress Level", + consistency: "Consistency", + + stages: { + beginner: "Beginner", + foundation: "Foundation", + growing: "Growing", + established: "Established", + advanced: "Advanced", + }, + + stage_prefix: "Stage", + onboarding_needed: "Complete onboarding to see your progress.", + loading: "Loading...", + }, + + // ----------------------------------------------------------------------- + // Rewards page + // ----------------------------------------------------------------------- + rewards: { + title: "Rewards", + empty_subtitle: "Your rewards will appear here as you practice.", + empty_message: "Complete your first practice to earn a reward.", + earned_one: "{count} reward earned", + earned_other: "{count} rewards earned", + }, + + // ----------------------------------------------------------------------- + // Summary page + // ----------------------------------------------------------------------- + summary: { + title: "Weekly summary", + week_of: "Week of {date}", + empty_subtitle: "Generated each week from your practice history", + empty_message: + "Your first weekly summary will be generated after you complete at least one practice session. Summaries run automatically each week.", + }, + + // ----------------------------------------------------------------------- + // Settings page + // ----------------------------------------------------------------------- + settings: { + title: "Settings", + + onboarding: { + title: "Welcome to Inner OS", + description: + "Inner OS generates one personalized mental training task each day, guided by your practice history and inner development stage. Your reflections shape what comes next. Everything is private — just you and your practice.", + cta: "Get started", + setting_up: "Setting up...", + done: "You're all set. Head home to start your practice.", + error: "Something went wrong. Try again.", + }, + + profile: { + title: "Profile", + signed_in_as: "Signed in as", + stage_and_sessions: "Stage {stage} · {count} sessions", + }, + + about: { + title: "About Inner OS", + p1: "Inner OS is a private inner development practice. You receive one personalized mental training task each day, tailored to your current state and development stage.", + p2: "Your reflections are private. Your data is yours. There are no leaderboards, no social features, no competition — just you and the work of becoming more aware, more stable, and more yourself.", + }, + + sign_out: "Sign out", + signing_out: "Signing out...", + }, + + // ----------------------------------------------------------------------- + // Practice page + // ----------------------------------------------------------------------- + practice: { + title: "Your practice", + why_prefix: "Why this task: ", + start: "Start", + finish_early: "Finish early", + complete: "Practice complete", + reflect: "Reflect on your practice", + }, + + // ----------------------------------------------------------------------- + // Reflection page + // ----------------------------------------------------------------------- + reflect: { + no_session: "No session to reflect on. Go home.", + title: "Reflect on your practice", + label: "What did you notice during your practice?", + placeholder: + "Were you focused or distracted? What emotions came up? Did anything surprise you?", + submit: "Complete practice", + submitting: "Saving...", + error: "Failed to save reflection", + generic_error: "Something went wrong", + + result_title: "Practice complete", + focus: "focus", + emotional_awareness: "emotional awareness", + back_home: "Back home", + }, + + // ----------------------------------------------------------------------- + // Sign-in page + // ----------------------------------------------------------------------- + sign_in: { + title: "Sign in", + subtitle: "Welcome back to Inner OS", + email: "Email", + password: "Password", + submit: "Sign in", + submitting: "Signing in...", + fallback_error: "Login failed", + network_error: "Network error. Try again.", + no_account: "No account?", + sign_up_link: "Sign up", + }, + + // ----------------------------------------------------------------------- + // Sign-up page + // ----------------------------------------------------------------------- + sign_up: { + title: "Sign up", + subtitle: "Start your inner development practice", + email: "Email", + password: "Password", + confirm_password: "Confirm password", + submit: "Create account", + submitting: "Creating account...", + password_mismatch: "Passwords do not match", + password_short: "Password must be at least 6 characters", + fallback_error: "Registration failed", + network_error: "Network error. Try again.", + has_account: "Already have an account?", + sign_in_link: "Sign in", + }, + + // ----------------------------------------------------------------------- + // Engine fallback strings (server-side, not React) + // ----------------------------------------------------------------------- + engine: { + reflection: { + keyword_blocked: "Thank you for sharing.", + no_reflection: "You completed your practice today.", + safety_blocked: "You completed your practice today.", + llm_failed: "You showed up today. That's what matters.", + }, + + summary: { + no_sessions: + "No completed practice sessions this week. Every week is a fresh start — your practice is here when you're ready.", + safety_fallback: "Your practice summary for this week.", + fallback_template: + "You completed {count} practice session{plural} this week. Showing up consistently is the foundation everything else is built on. Your current streak is {streak} day{streak_plural}. See you next week.", + }, + }, +}; + +export default en; diff --git a/dictionaries/zh.ts b/dictionaries/zh.ts new file mode 100644 index 0000000..3b4946c --- /dev/null +++ b/dictionaries/zh.ts @@ -0,0 +1,262 @@ +import type { Dict } from "@/lib/i18n/types"; + +const zh: Dict = { + // ----------------------------------------------------------------------- + // 导航 (TabBar) + // ----------------------------------------------------------------------- + nav: { + today: "今日", + progress: "进度", + rewards: "奖励", + summary: "周报", + settings: "设置", + }, + + // ----------------------------------------------------------------------- + // 共享标签 + // ----------------------------------------------------------------------- + shared: { + loading: "加载中...", + back: "← 返回", + + task_type: { + attention: "注意力", + emotion: "情绪", + stress: "压力", + clarity: "清晰度", + integration: "整合", + }, + + difficulty: { + beginner: "入门", + intermediate: "进阶", + advanced: "高级", + }, + + stage: { + beginner: "入门", + foundation: "基础", + growing: "成长", + established: "稳固", + advanced: "高级", + }, + + reward_type: { + daily_completion: "每日练习", + streak_3: "连续3天", + streak_7: "连续7天", + streak_14: "连续14天", + streak_30: "连续30天", + stage_up: "阶段提升", + first_session: "首次练习", + }, + }, + + // ----------------------------------------------------------------------- + // 首页 + // ----------------------------------------------------------------------- + home: { + greeting: { + morning: "早上好", + afternoon: "下午好", + evening: "晚上好", + fallback_name: "朋友", + subtitle: "你的练习在等你。", + }, + + onboarding: { + title: "还差一步", + description: "在设置中完成引导,开始你的练习之旅。", + cta: "前往设置", + }, + + stats: { + streak: "连续天数", + streak_unit: "天", + sessions: "练习次数", + sessions_unit: "次", + }, + + metrics: { + title: "核心指标", + attention: "注意力", + emotion: "情绪", + stress: "压力", + consistency: "持续性", + }, + + task: { + heading: "今日练习", + no_task: "今日还没有练习任务,生成一个开始吧。", + generate: "生成今日任务", + generating: "生成中...", + begin: "开始练习", + why_prefix: "", + }, + }, + + // ----------------------------------------------------------------------- + // 进度页 + // ----------------------------------------------------------------------- + progress: { + title: "我的进度", + metrics_title: "核心指标", + stage_title: "阶段进度", + + attention: "注意力", + emotional_stability: "情绪稳定性", + stress_level: "压力水平", + consistency: "持续性", + + stages: { + beginner: "入门", + foundation: "基础", + growing: "成长", + established: "稳固", + advanced: "高级", + }, + + stage_prefix: "阶段", + onboarding_needed: "完成引导后即可查看进度。", + loading: "加载中...", + }, + + // ----------------------------------------------------------------------- + // 奖励页 + // ----------------------------------------------------------------------- + rewards: { + title: "奖励", + empty_subtitle: "练习后奖励会在这里出现。", + empty_message: "完成第一次练习即可获得奖励。", + earned_one: "已获得 {count} 个奖励", + earned_other: "已获得 {count} 个奖励", + }, + + // ----------------------------------------------------------------------- + // 周报页 + // ----------------------------------------------------------------------- + summary: { + title: "每周总结", + week_of: "{date}当周", + empty_subtitle: "每周根据练习记录自动生成", + empty_message: "完成至少一次练习后,你的第一份每周总结将会生成。总结每周自动运行。", + }, + + // ----------------------------------------------------------------------- + // 设置页 + // ----------------------------------------------------------------------- + settings: { + title: "设置", + + onboarding: { + title: "欢迎来到 Inner OS", + description: + "Inner OS 每天为你生成一个个性化的心理训练任务,根据你的练习历史和内在发展阶段量身定制。你的反思会塑造接下来的内容。一切完全私密——只有你和你的练习。", + cta: "开始使用", + setting_up: "设置中...", + done: "一切就绪,返回首页开始练习吧。", + error: "出了点问题,请重试。", + }, + + profile: { + title: "个人信息", + signed_in_as: "已登录:", + stage_and_sessions: "阶段 {stage} · {count} 次练习", + }, + + about: { + title: "关于 Inner OS", + p1: "Inner OS 是一个私密的内在成长练习工具。每天根据你当前的状态和发展阶段,为你量身定制一个心理训练任务。", + p2: "你的反思是私密的。你的数据属于你。没有排行榜,没有社交功能,没有竞争——只有你和更加自觉、更加稳定、更加真实的自己。", + }, + + sign_out: "退出登录", + signing_out: "退出中...", + }, + + // ----------------------------------------------------------------------- + // 练习页 + // ----------------------------------------------------------------------- + practice: { + title: "你的练习", + why_prefix: "任务说明:", + start: "开始", + finish_early: "提前结束", + complete: "练习完成", + reflect: "记录感受", + }, + + // ----------------------------------------------------------------------- + // 反思页 + // ----------------------------------------------------------------------- + reflect: { + no_session: "没有可记录的练习,返回首页。", + title: "记录你的感受", + label: "练习中你注意到了什么?", + placeholder: "你专注还是走神了?出现了什么情绪?有什么让你意外的吗?", + submit: "完成练习", + submitting: "保存中...", + error: "保存失败", + generic_error: "出了点问题", + + result_title: "练习完成", + focus: "专注度", + emotional_awareness: "情绪觉察", + back_home: "返回首页", + }, + + // ----------------------------------------------------------------------- + // 登录页 + // ----------------------------------------------------------------------- + sign_in: { + title: "登录", + subtitle: "欢迎回到 Inner OS", + email: "邮箱", + password: "密码", + submit: "登录", + submitting: "登录中...", + fallback_error: "登录失败", + network_error: "网络错误,请重试。", + no_account: "还没有账号?", + sign_up_link: "注册", + }, + + // ----------------------------------------------------------------------- + // 注册页 + // ----------------------------------------------------------------------- + sign_up: { + title: "注册", + subtitle: "开始你的内在成长练习", + email: "邮箱", + password: "密码", + confirm_password: "确认密码", + submit: "创建账号", + submitting: "创建中...", + password_mismatch: "两次密码不一致", + password_short: "密码至少需要6位字符", + fallback_error: "注册失败", + network_error: "网络错误,请重试。", + has_account: "已有账号?", + sign_in_link: "登录", + }, + + // ----------------------------------------------------------------------- + // 引擎回退字符串 (服务端使用) + // ----------------------------------------------------------------------- + engine: { + reflection: { + keyword_blocked: "感谢你的分享。", + no_reflection: "你完成了今天的练习。", + safety_blocked: "你完成了今天的练习。", + llm_failed: "你今天来了,这本身就是最重要的。", + }, + + summary: { + no_sessions: "本周没有完成的练习。每一周都是全新的开始——你的练习随时等你回来。", + safety_fallback: "本周练习总结。", + fallback_template: "本周你完成了 {count} 次练习。持续出现是所有进步的基础。当前连续 {streak} 天。下周见。", + }, + }, +}; + +export default zh; diff --git a/package.json b/package.json index c8d25cb..4c8c21f 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,13 @@ "lint": "eslint" }, "dependencies": { - "@clerk/nextjs": "^7.3.7", + + "@prisma/adapter-better-sqlite3": "^7.8.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.10.0", + "jose": "^6.2.3", "next": "16.2.6", "openai": "^6.38.0", "react": "19.2.4", @@ -19,6 +23,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^3.0.0", "@types/node": "^20.19.41", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ebda23..f7210e9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,7 +4,7 @@ generator client { } datasource db { - provider = "postgresql" + provider = "sqlite" } enum TaskType { @@ -32,9 +32,10 @@ enum RewardEventType { } model User { - id String @id // Clerk user ID - email String @unique - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(uuid()) + email String @unique + passwordHash String? @map("password_hash") + createdAt DateTime @default(now()) @map("created_at") state UserState? @relation("UserState") sessions Session[] diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..59ffefd --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { verifyPassword, setAuthCookie } from "@/lib/auth"; + +export async function POST(req: Request) { + const body = await req.json().catch(() => null); + if (!body || typeof body !== "object") { + return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + } + + const { email, password } = body as { email?: string; password?: string }; + + if (!email || !password) { + return NextResponse.json( + { error: "Email and password are required" }, + { status: 400 } + ); + } + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user || !user.passwordHash) { + return NextResponse.json( + { error: "Invalid email or password" }, + { status: 401 } + ); + } + + const valid = await verifyPassword(password, user.passwordHash); + if (!valid) { + return NextResponse.json( + { error: "Invalid email or password" }, + { status: 401 } + ); + } + + await setAuthCookie(user.id); + + return NextResponse.json({ userId: user.id, email: user.email }); +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..4d285a8 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from "next/server"; +import { clearAuthCookie } from "@/lib/auth"; + +export async function POST() { + await clearAuthCookie(); + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..9b1a901 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { getSession } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + const session = await getSession(); + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.userId }, + select: { email: true }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json({ email: user.email }); +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..024063f --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { hashPassword, setAuthCookie } from "@/lib/auth"; + +export async function POST(req: Request) { + const body = await req.json().catch(() => null); + if (!body || typeof body !== "object") { + return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + } + + const { email, password } = body as { email?: string; password?: string }; + + if (!email || !password) { + return NextResponse.json( + { error: "Email and password are required" }, + { status: 400 } + ); + } + + if (typeof email !== "string" || !email.includes("@")) { + return NextResponse.json( + { error: "Invalid email address" }, + { status: 400 } + ); + } + + if (password.length < 6) { + return NextResponse.json( + { error: "Password must be at least 6 characters" }, + { status: 400 } + ); + } + + const existing = await prisma.user.findUnique({ where: { email } }); + if (existing) { + return NextResponse.json( + { error: "An account with this email already exists" }, + { status: 409 } + ); + } + + const passwordHash = await hashPassword(password); + const user = await prisma.user.create({ + data: { email, passwordHash }, + }); + + await setAuthCookie(user.id); + + return NextResponse.json({ userId: user.id, email: user.email }); +} diff --git a/src/app/api/cron/weekly-summaries/route.ts b/src/app/api/cron/weekly-summaries/route.ts index b06d74a..9e21add 100644 --- a/src/app/api/cron/weekly-summaries/route.ts +++ b/src/app/api/cron/weekly-summaries/route.ts @@ -19,7 +19,7 @@ export async function POST(req: Request) { } try { - const results = await generateAllWeeklySummaries(openai); + const results = await generateAllWeeklySummaries(openai, "zh"); const succeeded = results.filter((r) => r.success).length; const failed = results.filter((r) => !r.success).length; diff --git a/src/app/api/rewards/route.ts b/src/app/api/rewards/route.ts index 443e89c..bf20f34 100644 --- a/src/app/api/rewards/route.ts +++ b/src/app/api/rewards/route.ts @@ -1,10 +1,11 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; export async function GET(req: Request) { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const { searchParams } = new URL(req.url); const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50); diff --git a/src/app/api/session/complete/route.ts b/src/app/api/session/complete/route.ts index 5eba14a..ba990d2 100644 --- a/src/app/api/session/complete/route.ts +++ b/src/app/api/session/complete/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { openai } from "@/lib/openai"; import { prisma } from "@/lib/prisma"; @@ -7,8 +7,9 @@ import { computeStateUpdate } from "@/lib/engines/progress-tracker"; import { generateRewards, persistRewards } from "@/lib/engines/reward-generator"; export async function POST(req: Request) { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const body = await req.json().catch(() => null); if (!body || typeof body !== "object") { @@ -37,18 +38,30 @@ export async function POST(req: Request) { ); } - // Get current state - const currentState = await prisma.userState.findUnique({ + // Get or create user state (first session auto-creates defaults) + const currentState = await prisma.userState.upsert({ where: { userId }, + create: { + userId, + attentionLevel: 50, + emotionStability: 50, + stressLevel: 50, + practiceStage: 0, + consistencyScore: 0, + streakDays: 0, + totalSessionsCompleted: 0, + }, + update: {}, }); - if (!currentState) { - return NextResponse.json( - { error: "User state not found. Complete onboarding first." }, - { status: 400 } - ); - } + // Determine user's language preference from cookie + const cookieLang = req.headers + .get("cookie") + ?.split("; ") + .find((c) => c.startsWith("inneros-lang=")) + ?.split("=")[1]; + const locale: "zh" | "en" = cookieLang === "en" ? "en" : "zh"; // Process reflection through LLM #2 const reflection = await processReflection( @@ -63,7 +76,8 @@ export async function POST(req: Request) { emotionStability: currentState.emotionStability, stressLevel: currentState.stressLevel, }, - openai + openai, + locale ); // Get recent session dates for consistency computation diff --git a/src/app/api/summary/weekly/route.ts b/src/app/api/summary/weekly/route.ts index 6005fdc..7b3b7bb 100644 --- a/src/app/api/summary/weekly/route.ts +++ b/src/app/api/summary/weekly/route.ts @@ -1,11 +1,12 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import OpenAI from "openai"; import { prisma } from "@/lib/prisma"; export async function GET() { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const summary = await prisma.weeklySummary.findUnique({ where: { userId }, diff --git a/src/app/api/task/daily/route.ts b/src/app/api/task/daily/route.ts index 2219fcd..dad7258 100644 --- a/src/app/api/task/daily/route.ts +++ b/src/app/api/task/daily/route.ts @@ -1,10 +1,11 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; export async function GET() { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const today = new Date(); today.setHours(0, 0, 0, 0); diff --git a/src/app/api/task/generate/route.ts b/src/app/api/task/generate/route.ts index 6c90fb0..b2aa08f 100644 --- a/src/app/api/task/generate/route.ts +++ b/src/app/api/task/generate/route.ts @@ -1,11 +1,12 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { openai } from "@/lib/openai"; import { generateDailyTask } from "@/lib/engines/task-selector"; export async function POST() { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; try { const result = await generateDailyTask(userId, openai); diff --git a/src/app/api/user/onboarding/route.ts b/src/app/api/user/onboarding/route.ts index 18765f1..07d10ce 100644 --- a/src/app/api/user/onboarding/route.ts +++ b/src/app/api/user/onboarding/route.ts @@ -1,10 +1,11 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; export async function POST() { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; // Mark onboarding complete const state = await prisma.userState.upsert({ diff --git a/src/app/api/user/state/route.ts b/src/app/api/user/state/route.ts index af9d16a..edd4b57 100644 --- a/src/app/api/user/state/route.ts +++ b/src/app/api/user/state/route.ts @@ -1,10 +1,11 @@ -import { auth } from "@clerk/nextjs/server"; +import { getSession } from "@/lib/auth"; import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; export async function GET() { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const state = await prisma.userState.findUnique({ where: { userId }, @@ -35,8 +36,9 @@ export async function GET() { } export async function PATCH(req: Request) { - const { userId } = await auth(); - if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const session = await getSession(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const userId = session.userId; const body = await req.json().catch(() => null); if (!body || typeof body !== "object") { diff --git a/src/app/globals.css b/src/app/globals.css index f01f90c..7cf55a6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -44,3 +44,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +/* CJK font stack — wider characters read better at slightly smaller sizes */ +[lang="zh"] { + --font-display: "Noto Serif SC", "Source Han Serif SC", var(--font-fraunces), Georgia, serif; + --font-body: "Noto Sans SC", "Source Han Sans SC", "PingFang SC", "Microsoft YaHei", var(--font-dm-sans), system-ui, sans-serif; +} + +[lang="zh"] body { + font-size: 15px; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1fb18d8..f21e244 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,9 @@ import type { Metadata } from "next"; -import { ClerkProvider } from "@clerk/nextjs"; +import { cookies } from "next/headers"; import { DM_Sans, Fraunces } from "next/font/google"; import TabBar from "@/components/TabBar"; +import LanguageSwitcher from "@/components/LanguageSwitcher"; +import { I18nProvider } from "@/lib/i18n/context"; import "./globals.css"; const fraunces = Fraunces({ @@ -21,24 +23,29 @@ export const metadata: Metadata = { description: "Your private inner development practice.", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const cookieStore = await cookies(); + const langCookie = cookieStore.get("inneros-lang")?.value; + const lang = langCookie === "zh" || langCookie === "en" ? langCookie : "zh"; + return ( - - -
- {children} -
- + +
+ + {children} +
+ +
-
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 30c63f8..d004f71 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import { useUser } from "@clerk/nextjs"; +import { useAuth } from "@/hooks/use-auth"; import { useRouter } from "next/navigation"; import StatBar from "@/components/StatBar"; +import { useI18n } from "@/lib/i18n/context"; +import { formatTaskType, formatDifficulty } from "@/lib/i18n/utils"; interface StateData { attentionLevel: number; @@ -26,8 +28,9 @@ interface TaskData { } export default function HomePage() { - const { user, isLoaded: userLoaded } = useUser(); + const { user, isLoaded: userLoaded } = useAuth(); const router = useRouter(); + const { t } = useI18n(); const [state, setState] = useState(null); const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); @@ -69,7 +72,7 @@ export default function HomePage() { if (!userLoaded || loading) { return (
-
Loading...
+
{t("shared.loading")}
); } @@ -80,7 +83,7 @@ export default function HomePage() { if (state && !state.onboardingCompleted) { return (
- +

- One step left + {t("home.onboarding.title")}

- Complete onboarding in Settings to start your practice. + {t("home.onboarding.description")}

@@ -119,37 +122,37 @@ export default function HomePage() { const metrics = state ? [ - { label: "Attention", value: state.attentionLevel, color: "amber" as const }, - { label: "Emotion", value: state.emotionStability, color: "sage" as const }, - { label: "Stress", value: state.stressLevel, color: "warning" as const }, - { label: "Consistency", value: state.consistencyScore, color: "info" as const }, + { label: t("home.metrics.attention"), value: state.attentionLevel, color: "amber" as const }, + { label: t("home.metrics.emotion"), value: state.emotionStability, color: "sage" as const }, + { label: t("home.metrics.stress"), value: state.stressLevel, color: "warning" as const }, + { label: t("home.metrics.consistency"), value: state.consistencyScore, color: "info" as const }, ] : []; return (
- + {/* State overview */} {state && (

- Streak + {t("home.stats.streak")}

{state.streakDays}

-

days

+

{t("home.stats.streak_unit")}

- Sessions + {t("home.stats.sessions")}

{state.totalSessionsCompleted}

-

total

+

{t("home.stats.sessions_unit")}

)} @@ -158,7 +161,7 @@ export default function HomePage() { {state && (

- Core metrics + {t("home.metrics.title")}

{metrics.map((m) => (

- Today’s practice + {t("home.task.heading")}

{task ? (
- {formatTaskType(task.type)} + {formatTaskType(task.type, t)} - {formatDifficulty(task.difficulty)} · {task.duration}m + {formatDifficulty(task.difficulty, t)} · {task.duration}m

- {formatTaskType(task.type)} practice + {formatTaskType(task.type, t)} practice

{task.description} @@ -201,16 +204,16 @@ export default function HomePage() {

)}
) : (

- No task for today yet. Generate one to start your practice. + {t("home.task.no_task")}

{genError && (

{genError}

@@ -220,7 +223,7 @@ export default function HomePage() { disabled={generating} className="w-full py-3 px-6 rounded-lg bg-amber text-white font-medium text-base transition-all hover:bg-amber/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2 disabled:opacity-50" > - {generating ? "Generating..." : "Generate today's task"} + {generating ? t("home.task.generating") : t("home.task.generate")}
)} @@ -230,36 +233,19 @@ export default function HomePage() { } function Greeting({ firstName }: { firstName: string | null }) { + const { t } = useI18n(); const hour = new Date().getHours(); - const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening"; - const name = firstName || "there"; + const greetingKey = + hour < 12 ? "home.greeting.morning" : hour < 18 ? "home.greeting.afternoon" : "home.greeting.evening"; + const greeting = t(greetingKey); + const name = firstName || t("home.greeting.fallback_name"); return (

{greeting}, {name}

-

Your practice is waiting.

+

{t("home.greeting.subtitle")}

); } - -function formatTaskType(type: string): string { - const labels: Record = { - attention_training: "Attention", - emotion_awareness: "Emotion", - stress_regulation: "Stress", - cognitive_clarity: "Clarity", - integration: "Integration", - }; - return labels[type] ?? type; -} - -function formatDifficulty(difficulty: number): string { - const labels: Record = { - 1: "Beginner", - 2: "Intermediate", - 3: "Advanced", - }; - return labels[difficulty] ?? String(difficulty); -} diff --git a/src/app/practice/page.tsx b/src/app/practice/page.tsx index df79627..02eb04b 100644 --- a/src/app/practice/page.tsx +++ b/src/app/practice/page.tsx @@ -2,6 +2,8 @@ import { Suspense, useEffect, useState, useCallback } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { useI18n } from "@/lib/i18n/context"; +import { formatTaskType } from "@/lib/i18n/utils"; interface TaskData { id: string; @@ -31,6 +33,7 @@ function LoadingFallback() { function PracticeContent() { const searchParams = useSearchParams(); const router = useRouter(); + const { t } = useI18n(); const taskId = searchParams.get("id"); const [task, setTask] = useState(null); @@ -105,24 +108,24 @@ function PracticeContent() { onClick={() => router.push("/")} className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors" > - ← Back + {t("shared.back")} {/* Task display */}
- {formatTaskType(task.type)} + {formatTaskType(task.type, t)}

- Your practice + {t("practice.title")}

{task.description}

- Why this task: {task.reason} + {t("practice.why_prefix")}{task.reason}

@@ -134,7 +137,7 @@ function PracticeContent() { onClick={handleStart} className="w-32 h-32 rounded-full bg-amber text-white font-medium text-lg flex items-center justify-center transition-all hover:scale-105 active:scale-95 focus:outline-none focus:ring-4 focus:ring-amber/30" > - Start + {t("practice.start")} )} @@ -168,7 +171,7 @@ function PracticeContent() { onClick={handleFinish} className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors underline underline-offset-4" > - Finish early + {t("practice.finish_early")}
)} @@ -190,12 +193,12 @@ function PracticeContent() {
-

Practice complete

+

{t("practice.complete")}

)} @@ -203,14 +206,3 @@ function PracticeContent() { ); } - -function formatTaskType(type: string): string { - const labels: Record = { - attention_training: "Attention", - emotion_awareness: "Emotion", - stress_regulation: "Stress", - cognitive_clarity: "Clarity", - integration: "Integration", - }; - return labels[type] ?? type; -} diff --git a/src/app/progress/page.tsx b/src/app/progress/page.tsx index 16972be..424233c 100644 --- a/src/app/progress/page.tsx +++ b/src/app/progress/page.tsx @@ -1,9 +1,10 @@ "use client"; import { useEffect, useState } from "react"; -import { useUser } from "@clerk/nextjs"; +import { useAuth } from "@/hooks/use-auth"; import StatBar from "@/components/StatBar"; import StageBadge from "@/components/StageBadge"; +import { useI18n } from "@/lib/i18n/context"; interface StateData { attentionLevel: number; @@ -16,7 +17,8 @@ interface StateData { } export default function ProgressPage() { - const { isLoaded } = useUser(); + const { isLoaded } = useAuth(); + const { t } = useI18n(); const [state, setState] = useState(null); const [loading, setLoading] = useState(true); @@ -32,7 +34,7 @@ export default function ProgressPage() { if (loading) { return (
-
Loading...
+
{t("shared.loading")}
); } @@ -40,26 +42,26 @@ export default function ProgressPage() { if (!state) { return (
-

Complete onboarding to see your progress.

+

{t("progress.onboarding_needed")}

); } const metrics = [ - { label: "Attention", value: state.attentionLevel, color: "amber" as const }, - { label: "Emotional Stability", value: state.emotionStability, color: "sage" as const }, - { label: "Stress Level", value: state.stressLevel, color: "warning" as const }, - { label: "Consistency", value: state.consistencyScore, color: "info" as const }, + { label: t("progress.attention"), value: state.attentionLevel, color: "amber" as const }, + { label: t("progress.emotional_stability"), value: state.emotionStability, color: "sage" as const }, + { label: t("progress.stress_level"), value: state.stressLevel, color: "warning" as const }, + { label: t("progress.consistency"), value: state.consistencyScore, color: "info" as const }, ]; return (

- Your progress + {t("progress.title")}

- Stage + {t("progress.stage_prefix")}

@@ -67,28 +69,28 @@ export default function ProgressPage() {

- Streak + {t("home.stats.streak")}

{state.streakDays}

-

days

+

{t("home.stats.streak_unit")}

- Sessions + {t("home.stats.sessions")}

{state.totalSessionsCompleted}

-

total

+

{t("home.stats.sessions_unit")}

{/* Metric bars */}

- Core metrics + {t("progress.metrics_title")}

{metrics.map((m) => (

- Stage progress + {t("progress.stage_title")}

{[0, 1, 2, 3, 4].map((s) => ( @@ -118,11 +120,11 @@ export default function ProgressPage() { ))}
- Beginner - Foundation - Growing - Established - Advanced + {t("progress.stages.beginner")} + {t("progress.stages.foundation")} + {t("progress.stages.growing")} + {t("progress.stages.established")} + {t("progress.stages.advanced")}
diff --git a/src/app/reflect/page.tsx b/src/app/reflect/page.tsx index c6335b2..7c37aaa 100644 --- a/src/app/reflect/page.tsx +++ b/src/app/reflect/page.tsx @@ -2,10 +2,13 @@ import { useState, Suspense } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { useI18n } from "@/lib/i18n/context"; +import { formatTaskType } from "@/lib/i18n/utils"; function ReflectForm() { const searchParams = useSearchParams(); const router = useRouter(); + const { t } = useI18n(); const taskId = searchParams.get("taskId") || ""; const duration = parseInt(searchParams.get("duration") || "0", 10); @@ -39,13 +42,13 @@ function ReflectForm() { if (!res.ok) { const json = await res.json(); - throw new Error(json.error || "Failed to save reflection"); + throw new Error(json.error || t("reflect.error")); } const json = await res.json(); setResult(json.reflection); } catch (e) { - setError(e instanceof Error ? e.message : "Something went wrong"); + setError(e instanceof Error ? e.message : t("reflect.generic_error")); } finally { setSubmitting(false); } @@ -58,7 +61,7 @@ function ReflectForm() { onClick={() => router.push("/")} className="text-muted text-sm underline" > - No session to reflect on. Go home. + {t("reflect.no_session")} ); @@ -85,7 +88,7 @@ function ReflectForm() {

- Practice complete + {t("reflect.result_title")}

@@ -98,13 +101,13 @@ function ReflectForm() { {result.focusScore}/5 {" "} - focus + {t("reflect.focus")} {result.emotionScore}/5 {" "} - emotional awareness + {t("reflect.emotional_awareness")} @@ -113,7 +116,7 @@ function ReflectForm() { onClick={() => router.push("/")} className="w-full py-3 px-6 rounded-lg bg-ink dark:bg-[#F3EFE8] text-cream dark:text-[#1E1A14] font-medium text-base transition-all hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2" > - Back home + {t("reflect.back_home")} ); @@ -126,13 +129,13 @@ function ReflectForm() { onClick={() => router.back()} className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors" > - ← Back + {t("shared.back")}

- Reflect on your practice + {t("reflect.title")}

- {formatTaskType(taskType)} · {Math.floor(duration / 60)}m{" "} + {formatTaskType(taskType, t)} · {Math.floor(duration / 60)}m{" "} {duration % 60}s

@@ -142,14 +145,14 @@ function ReflectForm() { htmlFor="reflection-notes" className="block text-sm font-medium text-muted" > - What did you notice during your practice? + {t("reflect.label")}