Compare commits

..

No commits in common. "64669e5f588d8b54013b54e31c5c2ff0b8e651e5" and "fe038077397bec1d271c856f072fa833de47ee47" have entirely different histories.

10 changed files with 78 additions and 2081 deletions

View File

@ -11,11 +11,11 @@
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"components": "~/components",
"utils": "~/lib/utils",
"ui": "~/components/ui",
"lib": "~/lib",
"hooks": "~/hooks"
},
"iconLibrary": "lucide"
}

570
package-lock.json generated
View File

@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@t3-oss/env-nextjs": "^0.12.0",
@ -20,7 +19,6 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"recharts": "^2.15.3",
"tailwind-merge": "^3.2.0",
"zod": "^3.24.3"
},
@ -54,18 +52,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
@ -897,99 +883,6 @@
"node": ">=12.4.0"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.8.tgz",
"integrity": "sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collapsible": "1.1.8",
"@radix-ui/react-collection": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.8.tgz",
"integrity": "sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz",
"integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -1005,54 +898,6 @@
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz",
@ -1076,30 +921,6 @@
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
@ -1141,58 +962,6 @@
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -1554,69 +1323,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -2666,129 +2372,9 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -2868,12 +2454,6 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -2940,16 +2520,6 @@
"node": ">=0.10.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -3599,12 +3169,6 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"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",
@ -3612,15 +3176,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@ -4082,15 +3637,6 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -4532,6 +4078,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -4896,12 +4443,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -4913,6 +4454,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@ -5121,6 +4663,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -5514,6 +5057,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@ -5593,75 +5137,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/recharts": {
"version": "2.15.3",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
"integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.4",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/recharts/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/reflect.getprototypeof": {
@ -5687,12 +5163,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@ -6299,12 +5769,6 @@
"node": ">=6"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
@ -6602,28 +6066,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -17,7 +17,6 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-accordion": "^1.2.8",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@t3-oss/env-nextjs": "^0.12.0",
@ -28,7 +27,6 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.1",
"recharts": "^2.15.3",
"tailwind-merge": "^3.2.0",
"zod": "^3.24.3"
},

View File

@ -1,639 +0,0 @@
"use client";
import * as React from "react";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import {
Area,
AreaChart,
CartesianGrid,
XAxis,
YAxis,
ReferenceLine,
} from "recharts";
// Schema for form validation
const formSchema = z.object({
startingCapital: z.coerce
.number()
.min(0, "Starting capital must be a non-negative number"),
monthlySavings: z.coerce
.number()
.min(0, "Monthly savings must be a non-negative number"),
currentAge: z.coerce.number().min(18, "Age must be at least 18"),
cagr: z.coerce.number().min(0, "Growth rate must be a non-negative number"),
desiredMonthlyAllowance: z.coerce
.number()
.min(0, "Monthly allowance must be a non-negative number"),
inflationRate: z.coerce
.number()
.min(0, "Inflation rate must be a non-negative number"),
lifeExpectancy: z.coerce
.number()
.min(50, "Life expectancy must be at least 50"),
});
// Type for form values
type FormValues = z.infer<typeof formSchema>;
interface CalculationResult {
fireNumber: number | null;
retirementAge: number | null;
inflationAdjustedAllowance: number | null;
retirementYears: number | null;
error?: string;
yearlyData?: Array<{
age: number;
year: number;
balance: number;
phase: "accumulation" | "retirement";
}>;
}
export default function FireCalculatorForm() {
const [result, setResult] = useState<CalculationResult | null>(null);
const currentYear = new Date().getFullYear();
// Initialize form with default values
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
startingCapital: 50000,
monthlySavings: 1500,
currentAge: 25,
cagr: 7,
desiredMonthlyAllowance: 2000,
inflationRate: 2,
lifeExpectancy: 84,
},
});
function onSubmit(values: FormValues) {
setResult(null); // Reset previous results
const startingCapital = values.startingCapital;
const monthlySavings = values.monthlySavings;
const currentAge = values.currentAge;
const annualGrowthRate = values.cagr / 100;
const initialMonthlyAllowance = values.desiredMonthlyAllowance;
const annualInflation = values.inflationRate / 100;
const lifeExpectancy = values.lifeExpectancy;
const monthlyGrowthRate = Math.pow(1 + annualGrowthRate, 1 / 12) - 1;
const monthlyInflationRate = Math.pow(1 + annualInflation, 1 / 12) - 1;
const maxIterations = 1000; // Safety limit for iterations
// Binary search for the required retirement capital
let low = initialMonthlyAllowance * 12; // Minimum: one year of expenses
let high = initialMonthlyAllowance * 12 * 100; // Maximum: hundred years of expenses
let requiredCapital = 0;
let retirementAge = 0;
let finalInflationAdjustedAllowance = 0;
// First, find when retirement is possible with accumulation phase
let canRetire = false;
let currentCapital = startingCapital;
let age = currentAge;
let monthlyAllowance = initialMonthlyAllowance;
let iterations = 0;
// Array to store yearly data for the chart
const yearlyData: CalculationResult["yearlyData"] = [];
// Add starting point
yearlyData.push({
age: currentAge,
year: currentYear,
balance: startingCapital,
phase: "accumulation",
});
// Accumulation phase simulation
while (age < lifeExpectancy && iterations < maxIterations) {
// Simulate one year of saving and growth
for (let month = 0; month < 12; month++) {
currentCapital += monthlySavings;
currentCapital *= 1 + monthlyGrowthRate;
// Update allowance for inflation
monthlyAllowance *= 1 + monthlyInflationRate;
}
age++;
iterations++;
// Record yearly data
yearlyData.push({
age: age,
year: currentYear + (age - currentAge),
balance: Math.round(currentCapital),
phase: "accumulation",
});
// Check each possible retirement capital target through binary search
const mid = (low + high) / 2;
if (high - low < 1) {
// Binary search converged
requiredCapital = mid;
break;
}
// Test if this retirement capital is sufficient
let testCapital = mid;
let testAge = age;
let testAllowance = monthlyAllowance;
let isSufficient = true;
// Simulate retirement phase with this capital
while (testAge < lifeExpectancy) {
for (let month = 0; month < 12; month++) {
// Withdraw inflation-adjusted allowance
testCapital -= testAllowance;
// Grow remaining capital
testCapital *= 1 + monthlyGrowthRate;
// Adjust allowance for inflation
testAllowance *= 1 + monthlyInflationRate;
}
testAge++;
// Check if we've depleted capital before life expectancy
if (testCapital <= 0) {
isSufficient = false;
break;
}
}
if (isSufficient) {
high = mid; // This capital or less might be enough
if (currentCapital >= mid) {
// We can retire now with this capital
canRetire = true;
retirementAge = age;
requiredCapital = mid;
finalInflationAdjustedAllowance = monthlyAllowance;
break;
}
} else {
low = mid; // We need more capital
}
}
// If we didn't find retirement possible in the loop
if (!canRetire && iterations < maxIterations) {
// Continue accumulation phase until we reach sufficient capital
while (age < lifeExpectancy && iterations < maxIterations) {
// Simulate one year
for (let month = 0; month < 12; month++) {
currentCapital += monthlySavings;
currentCapital *= 1 + monthlyGrowthRate;
monthlyAllowance *= 1 + monthlyInflationRate;
}
age++;
iterations++;
// Record yearly data
yearlyData.push({
age: age,
year: currentYear + (age - currentAge),
balance: Math.round(currentCapital),
phase: "accumulation",
});
// Test with current capital
let testCapital = currentCapital;
let testAge = age;
let testAllowance = monthlyAllowance;
let isSufficient = true;
// Simulate retirement with current capital
while (testAge < lifeExpectancy) {
for (let month = 0; month < 12; month++) {
testCapital -= testAllowance;
testCapital *= 1 + monthlyGrowthRate;
testAllowance *= 1 + monthlyInflationRate;
}
testAge++;
if (testCapital <= 0) {
isSufficient = false;
break;
}
}
if (isSufficient) {
canRetire = true;
retirementAge = age;
requiredCapital = currentCapital;
finalInflationAdjustedAllowance = monthlyAllowance;
break;
}
}
}
// If retirement is possible, simulate the retirement phase for the chart
if (canRetire) {
// Update the phase for all years after retirement
yearlyData.forEach((data) => {
if (data.age >= retirementAge) {
data.phase = "retirement";
}
});
// Continue simulation for retirement phase if needed
let simulationCapital = currentCapital;
let simulationAllowance = monthlyAllowance;
let simulationAge = age;
// If we haven't simulated up to life expectancy, continue
while (simulationAge < lifeExpectancy) {
for (let month = 0; month < 12; month++) {
simulationCapital -= simulationAllowance;
simulationCapital *= 1 + monthlyGrowthRate;
simulationAllowance *= 1 + monthlyInflationRate;
}
simulationAge++;
// Record yearly data
yearlyData.push({
age: simulationAge,
year: currentYear + (simulationAge - currentAge),
balance: Math.round(simulationCapital),
phase: "retirement",
});
}
}
if (canRetire) {
setResult({
fireNumber: requiredCapital,
retirementAge: retirementAge,
inflationAdjustedAllowance: finalInflationAdjustedAllowance,
retirementYears: lifeExpectancy - retirementAge,
yearlyData: yearlyData,
error: undefined,
});
} else {
setResult({
fireNumber: null,
retirementAge: null,
inflationAdjustedAllowance: null,
retirementYears: null,
yearlyData: yearlyData,
error:
iterations >= maxIterations
? "Calculation exceeded maximum iterations."
: "Cannot reach FIRE goal before life expectancy with current parameters.",
});
}
}
// Helper function to format currency without specific symbols
const formatNumber = (value: number | null) => {
if (value === null) return "N/A";
return new Intl.NumberFormat("en", {
maximumFractionDigits: 0,
}).format(value);
};
return (
<div className="w-full max-w-3xl">
<Card className="mb-8">
<CardHeader>
<CardTitle className="text-2xl">FIRE Calculator</CardTitle>
<CardDescription>
Calculate your path to financial independence and retirement
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<FormField
control={form.control}
name="startingCapital"
render={({ field }) => (
<FormItem>
<FormLabel>Starting Capital</FormLabel>
<FormControl>
<Input
placeholder="e.g., 10000"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="monthlySavings"
render={({ field }) => (
<FormItem>
<FormLabel>Monthly Savings</FormLabel>
<FormControl>
<Input
placeholder="e.g., 500"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="currentAge"
render={({ field }) => (
<FormItem>
<FormLabel>Current Age</FormLabel>
<FormControl>
<Input
placeholder="e.g., 30"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cagr"
render={({ field }) => (
<FormItem>
<FormLabel>Expected Annual Growth Rate (%)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 7"
type="number"
step="0.1"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="desiredMonthlyAllowance"
render={({ field }) => (
<FormItem>
<FormLabel>
Desired Monthly Allowance (Today&apos;s Value)
</FormLabel>
<FormControl>
<Input
placeholder="e.g., 2000"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="inflationRate"
render={({ field }) => (
<FormItem>
<FormLabel>Annual Inflation Rate (%)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 2"
type="number"
step="0.1"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lifeExpectancy"
render={({ field }) => (
<FormItem>
<FormLabel>Life Expectancy (Age)</FormLabel>
<FormControl>
<Input
placeholder="e.g., 90"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full">
Calculate
</Button>
</form>
</Form>
</CardContent>
</Card>
{result && (
<>
<Card className="mb-8">
<CardHeader>
<CardTitle>Results</CardTitle>
</CardHeader>
<CardContent>
{result.error ? (
<p className="text-destructive">{result.error}</p>
) : (
<div className="space-y-4">
<div>
<Label>FIRE Number (Required Capital)</Label>
<p className="text-2xl font-bold">
{formatNumber(result.fireNumber)}
</p>
</div>
<div>
<Label>Estimated Retirement Age</Label>
<p className="text-2xl font-bold">
{result.retirementAge ?? "N/A"}
</p>
</div>
{result.inflationAdjustedAllowance && (
<div>
<Label>
Monthly Allowance at Retirement (Inflation Adjusted)
</Label>
<p className="text-2xl font-bold">
{formatNumber(result.inflationAdjustedAllowance)}
</p>
</div>
)}
{result.retirementYears && (
<div>
<Label>Retirement Duration (Years)</Label>
<p className="text-2xl font-bold">
{result.retirementYears}
</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
{result && result.yearlyData && result.yearlyData.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Financial Projection</CardTitle>
<CardDescription>
Projected balance growth and FIRE number threshold
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
className="h-80"
config={{
balance: {
label: "Balance",
color: "var(--chart-1)",
},
fireNumber: {
label: "FIRE Number",
color: "var(--chart-3)",
},
}}
>
<AreaChart
data={result.yearlyData}
margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="year"
label={{
value: "Year",
position: "insideBottom",
offset: -10,
}}
/>
<YAxis
tickFormatter={(value: number) => {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
} else if (value >= 1000) {
return `${(value / 1000).toFixed(0)}K`;
}
return `${value}`;
}}
width={80}
/>
<ChartTooltip
content={({ active, payload }) => {
if (active && payload?.[0]?.payload) {
const data = payload[0]
.payload as (typeof result.yearlyData)[0];
return (
<div className="bg-background border p-2 shadow-sm">
<p className="font-medium">{`Year: ${data.year} (Age: ${data.age})`}</p>
<p className="text-primary">{`Balance: ${formatNumber(data.balance)}`}</p>
{result.fireNumber && (
<p className="text-destructive">{`FIRE Number: ${formatNumber(result.fireNumber)}`}</p>
)}
<p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p>
</div>
);
}
return null;
}}
/>
<defs>
<linearGradient
id="fillBalance"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="var(--chart-1)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--chart-1)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="balance"
name="balance"
stroke="var(--chart-1)"
fill="url(#fillBalance)"
fillOpacity={0.4}
activeDot={{ r: 6 }}
/>
{result.fireNumber && (
<ReferenceLine
y={result.fireNumber}
stroke="var(--chart-3)"
strokeWidth={2}
strokeDasharray="5 5"
label={{
value: "FIRE Number",
position: "insideBottomRight",
}}
/>
)}
{result.retirementAge && (
<ReferenceLine
x={
currentYear +
(result.retirementAge - form.getValues().currentAge)
}
stroke="var(--chart-2)"
strokeWidth={2}
label={{
value: "Retirement",
position: "insideTopRight",
}}
/>
)}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)}
</>
)}
</div>
);
}

View File

@ -1,27 +0,0 @@
import "@/styles/globals.css";
import { type Metadata } from "next";
import { Geist } from "next/font/google";
export const metadata: Metadata = {
title:
"FIRE Calculator - Plan Your Financial Independence & Early Retirement",
description:
"Calculate your FIRE number, estimate your retirement age, and plan your path to financial independence with this comprehensive FIRE calculator.",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geist = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
});
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={geist.variable}>
<body>{children}</body>
</html>
);
}

View File

@ -1,348 +0,0 @@
import FireCalculatorForm from "./components/FireCalculatorForm";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
export default function HomePage() {
return (
<main className="text-primary-foreground to-destructive from-secondary flex min-h-screen flex-col items-center bg-gradient-to-b p-4">
<div className="container mx-auto flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="text-primary-foreground text-5xl font-extrabold tracking-tight sm:text-[5rem]">
FIRE Calculator
</h1>
<FireCalculatorForm />
</div>
{/* Added SEO Content Sections */}
<div className="container mx-auto max-w-4xl px-4 py-8 text-left">
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">What is FIRE?</h2>
<p className="mb-4 text-lg leading-relaxed">
FIRE stands for &quot;Financial Independence, Retire Early.&quot;
It&apos;s a movement focused on aggressive saving and investing to
build a large enough portfolio that the returns can cover living
expenses indefinitely. Achieving FIRE means you are no longer
dependent on traditional employment to fund your lifestyle, giving
you the freedom to pursue passions, travel, or simply enjoy life
without the need for a regular paycheck.
</p>
<p className="text-lg leading-relaxed">
The core principle often involves saving a high percentage of income
(sometimes 50% or more) and investing it wisely, typically in
low-cost index funds. The target amount, often called the &quot;FIRE
number,&quot; is usually calculated as 25 times your desired annual
spending, based on the 4% safe withdrawal rate rule.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">How This Calculator Works</h2>
<p className="mb-4 text-lg leading-relaxed">
This calculator helps you estimate your path to FIRE based on your
current financial situation and future projections. Here&apos;s a
breakdown of the inputs:
</p>
<ul className="mb-4 ml-6 list-disc space-y-2 text-lg">
<li>
<strong>Starting Capital:</strong> The total amount you currently
have invested.
</li>
<li>
<strong>Monthly Savings:</strong> The amount you consistently save
and invest each month.
</li>
<li>
<strong>Current Age:</strong> Your current age in years.
</li>
<li>
<strong>Expected Annual Growth Rate (%):</strong> The average
annual return you expect from your investments (after fees, before
inflation).
</li>
<li>
<strong>Desired Monthly Allowance (Today&apos;s Value):</strong>{" "}
How much you want to be able to spend each month in retirement, in
today&apos;s money value.
</li>
<li>
<strong>Annual Inflation Rate (%):</strong> The expected average
rate at which the cost of living will increase.
</li>
<li>
<strong>Life Expectancy (Age):</strong> The age until which you
want your funds to last.
</li>
</ul>
<p className="text-lg leading-relaxed">
The calculator simulates your investment growth year by year,
factoring in monthly contributions, compound growth, and
inflation&apos;s effect on your target allowance. It then determines
the age at which your accumulated capital is sufficient to sustain
your desired, inflation-adjusted monthly allowance throughout your
expected retirement years until your specified life expectancy. It
estimates your &quot;FIRE Number&quot; (the capital needed at
retirement) and the age you might reach it.
</p>
</section>
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">
Frequently Asked Questions (FAQ)
</h2>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger className="text-xl font-semibold">
What is the 4% rule?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
The 4% rule is a guideline suggesting that you can safely
withdraw 4% of your investment portfolio&apos;s value in your
first year of retirement, and then adjust that amount for
inflation each subsequent year, with a high probability of your
money lasting for at least 30 years. This calculator uses a more
dynamic simulation based on your life expectancy but is related
to this concept.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger className="text-xl font-semibold">
Is the Expected Growth Rate realistic?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Historically, diversified stock market investments have returned
around 7-10% annually over the long term, before inflation. A
rate of 7% (after fees) is often used as a reasonable estimate,
but past performance doesn&apos;t guarantee future results.
It&apos;s crucial to choose a rate you feel comfortable with and
understand the associated risks.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger className="text-xl font-semibold">
How does inflation impact my FIRE number?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Inflation erodes the purchasing power of money over time. Your
desired monthly allowance needs to increase each year just to
maintain the same standard of living. This calculator accounts
for this by adjusting your target allowance upwards based on the
inflation rate you provide, ensuring the calculated FIRE number
supports your desired lifestyle in future dollars.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4">
<AccordionTrigger className="text-xl font-semibold">
Can I really retire early?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
Retiring significantly earlier than traditional retirement age
is possible but requires discipline, a high savings rate, and
consistent investment growth. The feasibility depends heavily on
your income, expenses, savings habits, and investment returns.
Use this calculator as a tool for planning and motivation, but
remember it provides estimates based on your inputs.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5">
<AccordionTrigger className="text-xl font-semibold">
What does FIRE stand for?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
FIRE stands for Financial Independence, Retire Early. It
represents a lifestyle movement aimed at maximizing your savings
rate through increased income and/or decreased expenses to
achieve financial independence and retire much earlier than
traditional retirement age.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-6">
<AccordionTrigger className="text-xl font-semibold">
How much should I save each month?
</AccordionTrigger>
<AccordionContent className="text-lg leading-relaxed">
FIRE enthusiasts typically aim to save 50-70% of their income.
The more you can save, the faster you&apos;ll reach your FIRE
goal. However, the right amount depends on your income,
lifestyle, and target retirement age. Use the calculator to
experiment with different monthly savings amounts to see their
impact on your retirement timeline.
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
{/* Optional: Add a section for relevant resources/links here */}
<section className="mb-12">
<h2 className="mb-4 text-3xl font-bold">
Further Reading & Resources
</h2>
<p className="mb-6 text-lg leading-relaxed">
Want to learn more about FIRE and continue your journey to financial
independence? Here are some valuable resources to explore:
</p>
<div className="bg-secondary/20 my-8 rounded-md p-4 text-lg">
<p className="font-semibold">Getting Started with FIRE:</p>
<ol className="ml-6 list-decimal space-y-1">
<li>
Read foundational content like Mr. Money Mustache&apos;s simple
math article
</li>
<li>
Calculate your personal numbers using this and other FIRE
calculators
</li>
<li>
Join communities like r/Fire to ask questions and find support
</li>
<li>Explore books and podcasts to deepen your understanding</li>
</ol>
</div>
<div className="grid gap-8 md:grid-cols-2">
<div>
<h3 className="mb-3 text-xl font-semibold">Blogs & Websites</h3>
<ul className="ml-6 list-disc space-y-2 text-lg">
<li>
<a
href="https://www.mrmoneymustache.com/2012/01/13/the-shockingly-simple-math-behind-early-retirement/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Mr. Money Mustache - The Shockingly Simple Math Behind Early
Retirement
</a>
</li>
<li>
<a
href="https://www.playingwithfire.co/resources"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Playing With FIRE - Comprehensive Resources
</a>
</li>
<li>
<a
href="https://www.reddit.com/r/Fire/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
r/Fire Reddit Community
</a>
</li>
</ul>
</div>
<div>
<h3 className="mb-3 text-xl font-semibold">Books & Learning</h3>
<ul className="ml-6 list-disc space-y-2 text-lg">
<li>
<a
href="https://www.amazon.com/Your-Money-Life-Transforming-Relationship/dp/0143115766"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Your Money or Your Life - Vicki Robin & Joe Dominguez
</a>
</li>
<li>
<a
href="https://www.playingwithfire.co/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Playing With FIRE Documentary
</a>
</li>
<li>
<a
href="https://podcasts.apple.com/us/podcast/can-you-retire-now-this-fire-calculator-will-tell-you/id1330225136?i=1000683436292"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
BiggerPockets Money Podcast - FIRE Calculators
</a>
</li>
</ul>
</div>
<div>
<h3 className="mb-3 text-xl font-semibold">
Additional Calculators & Tools
</h3>
<ul className="ml-6 list-disc space-y-2 text-lg">
<li>
<a
href="https://walletburst.com/tools/coast-fire-calculator/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Coast FIRE Calculator - For those considering a partial
early retirement
</a>
</li>
<li>
<a
href="https://www.empower.com/retirement-calculator"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Empower Retirement Planner - Free portfolio analysis and net
worth tracking
</a>
</li>
</ul>
</div>
<div>
<h3 className="mb-3 text-xl font-semibold">
Recent Articles & Trends
</h3>
<ul className="ml-6 list-disc space-y-2 text-lg">
<li>
<a
href="https://www.businessinsider.com/retiring-tech-early-coast-fire-make-me-millionaire-2025-4"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Coast FIRE: Retiring in your 30s while becoming a
millionaire by 60
</a>
</li>
<li>
<a
href="https://www.businessinsider.com/financial-independence-retire-early-saving-loneliness-retreat-bali-making-friends-2025-2"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
The Social Side of FIRE: Finding Community in Financial
Independence
</a>
</li>
</ul>
</div>
</div>
</section>
</div>
</main>
);
}

View File

@ -1,69 +0,0 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn(
"border-primary-foreground/20 border-b last:border-b-0",
className,
)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -1,353 +0,0 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs))
}

View File

@ -4,8 +4,7 @@
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans:
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@ -49,77 +48,71 @@
:root {
--radius: 0.625rem;
--background: oklch(0.97 0.0228 95.96); /* cosmic latte */
--foreground: oklch(0.39 0.0215 96.47); /* black olive */
--card: oklch(0.97 0.0228 95.96); /* cosmic latte */
--card-foreground: oklch(0.39 0.0215 96.47); /* black olive */
--popover: oklch(0.97 0.0228 95.96); /* cosmic latte */
--popover-foreground: oklch(0.39 0.0215 96.47); /* black olive */
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--secondary: oklch(0.49 0.1326 259.29); /* denim */
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--muted: oklch(0.67 0.0763 198.81 / 20%); /* verdigris with opacity */
--muted-foreground: oklch(
0.39 0.0215 96.47 / 80%
); /* black olive with opacity */
--accent: oklch(0.49 0.1326 259.29); /* denim */
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
--border: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
--input: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
--chart-4: oklch(0.39 0.0215 96.47); /* black olive */
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
--sidebar: oklch(0.49 0.1326 259.29 / 10%); /* denim with opacity */
--sidebar-foreground: oklch(0.39 0.0215 96.47); /* black olive */
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--sidebar-border: oklch(
0.67 0.0763 198.81 / 20%
); /* verdigris with opacity */
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.39 0.0215 96.47); /* black olive */
--foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--card: oklch(0.39 0.0215 96.47 / 80%); /* black olive with opacity */
--card-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--popover: oklch(0.39 0.0215 96.47); /* black olive */
--popover-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--secondary: oklch(0.49 0.1326 259.29); /* denim */
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--muted: oklch(0.39 0.0215 96.47 / 70%); /* black olive with opacity */
--muted-foreground: oklch(0.67 0.0763 198.81); /* verdigris */
--accent: oklch(0.49 0.1326 259.29); /* denim */
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
--border: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
--input: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
--chart-4: oklch(0.97 0.0228 95.96); /* cosmic latte */
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
--sidebar: oklch(0.39 0.0215 96.47 / 90%); /* black olive with opacity */
--sidebar-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
--sidebar-border: oklch(
0.97 0.0228 95.96 / 10%
); /* cosmic latte with opacity */
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {