Fix CI: Update bun.lock and fix linting errors
This commit is contained in:
parent
c339105875
commit
2fefb64710
75
bun.lock
75
bun.lock
|
|
@ -42,6 +42,7 @@
|
|||
"lucide-react": "^0.553.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.5.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.0",
|
||||
|
|
@ -337,6 +338,8 @@
|
|||
|
||||
"@react-types/shared": ["@react-types/shared@3.31.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="],
|
||||
|
|
@ -379,6 +382,10 @@
|
|||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="],
|
||||
|
|
@ -427,6 +434,24 @@
|
|||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-logic-js": ["@types/json-logic-js@2.0.8", "", {}, "sha512-WgNsDPuTPKYXl0Jh0IfoCoJoAGGYZt5qzpmjuLSEg7r0cKp/kWtWp0HAsVepyPSPyXiHo6uXp/B/kW/2J1fa2Q=="],
|
||||
|
|
@ -443,6 +468,8 @@
|
|||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.1", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/type-utils": "8.39.1", "@typescript-eslint/utils": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg=="],
|
||||
|
|
@ -557,6 +584,28 @@
|
|||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
|
@ -567,6 +616,8 @@
|
|||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
|
@ -609,6 +660,8 @@
|
|||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.42.0", "", {}, "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": "bin/esbuild" }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
|
@ -637,6 +690,8 @@
|
|||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
|
@ -719,6 +774,8 @@
|
|||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
|
@ -727,6 +784,8 @@
|
|||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"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" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
|
@ -941,6 +1000,8 @@
|
|||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
|
|
@ -949,12 +1010,20 @@
|
|||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"recharts": ["recharts@3.5.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA=="],
|
||||
|
||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||
|
||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||
|
||||
"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" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||
|
||||
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
|
@ -1035,6 +1104,8 @@
|
|||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
|
@ -1079,6 +1150,8 @@
|
|||
|
||||
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "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" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
|
||||
|
||||
"vite-plugin-full-reload": ["vite-plugin-full-reload@1.2.0", "", { "dependencies": { "picocolors": "^1.0.0", "picomatch": "^2.3.1" } }, "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA=="],
|
||||
|
|
@ -1145,6 +1218,8 @@
|
|||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@reduxjs/toolkit/immer": ["immer@11.0.1", "", {}, "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
|
||||
--font-mono: "Google Sans Code", monospace;
|
||||
--font-mono: 'Google Sans Code', monospace;
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
|
|
@ -180,7 +180,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.font-heading {
|
||||
font-family: 'Stack Sans Text', ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import '../css/app.css';
|
|||
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from 'lucide-react';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Toaster } from 'sonner';
|
||||
|
|
@ -9,7 +16,6 @@ import { EncryptionKeyProvider } from './contexts/encryption-key-context';
|
|||
import { SyncProvider } from './contexts/sync-context';
|
||||
import { initializeTheme } from './hooks/use-appearance';
|
||||
import type { SharedData } from './types';
|
||||
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from 'lucide-react';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
|
||||
|
|
@ -35,13 +41,20 @@ createInertiaApp({
|
|||
>
|
||||
<App {...props} />
|
||||
<div className="[&_[data-sonner-toaster]]:!top-4 [&_[data-sonner-toaster]]:!left-1/2 [&_[data-sonner-toaster]]:!-translate-x-1/2 [&_[data-sonner-toaster]]:md:!top-auto [&_[data-sonner-toaster]]:md:!right-4 [&_[data-sonner-toaster]]:md:!bottom-4 [&_[data-sonner-toaster]]:md:!left-auto [&_[data-sonner-toaster]]:md:!translate-x-0">
|
||||
<Toaster richColors
|
||||
<Toaster
|
||||
richColors
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
success: (
|
||||
<CircleCheckIcon className="size-4" />
|
||||
),
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
warning: (
|
||||
<TriangleAlertIcon className="size-4" />
|
||||
),
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
loading: (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { BirdIcon, SVGAttributes } from 'lucide-react';
|
||||
|
||||
export default function AppLogoIcon({ className }: { className?: SVGAttributes['className'] }) {
|
||||
export default function AppLogoIcon({
|
||||
className,
|
||||
}: {
|
||||
className?: SVGAttributes['className'];
|
||||
}) {
|
||||
return (
|
||||
<BirdIcon className={cn(
|
||||
"size-5 text-[#1b1b18] dark:text-[#EDEDEC]",
|
||||
className,
|
||||
'fill-transparent'
|
||||
)} />
|
||||
<BirdIcon
|
||||
className={cn(
|
||||
'size-5 text-[#1b1b18] dark:text-[#EDEDEC]',
|
||||
className,
|
||||
'fill-transparent',
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,48 +146,45 @@ export function RuleBuilder({ value, onChange, error }: RuleBuilderProps) {
|
|||
|
||||
<div className="space-y-2">
|
||||
{group.conditions.map((condition) => (
|
||||
<div key={condition.id}>
|
||||
<ConditionRow
|
||||
condition={condition}
|
||||
onChange={(
|
||||
updatedCondition,
|
||||
) => {
|
||||
updateGroup(group.id, {
|
||||
...group,
|
||||
conditions:
|
||||
group.conditions.map(
|
||||
(c) =>
|
||||
c.id ===
|
||||
condition.id
|
||||
? updatedCondition
|
||||
: c,
|
||||
),
|
||||
});
|
||||
}}
|
||||
onRemove={() => {
|
||||
if (
|
||||
group.conditions
|
||||
.length === 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
updateGroup(group.id, {
|
||||
...group,
|
||||
conditions:
|
||||
group.conditions.filter(
|
||||
(c) =>
|
||||
c.id !==
|
||||
condition.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
canRemove={
|
||||
group.conditions.length > 1
|
||||
<div key={condition.id}>
|
||||
<ConditionRow
|
||||
condition={condition}
|
||||
onChange={(updatedCondition) => {
|
||||
updateGroup(group.id, {
|
||||
...group,
|
||||
conditions:
|
||||
group.conditions.map(
|
||||
(c) =>
|
||||
c.id ===
|
||||
condition.id
|
||||
? updatedCondition
|
||||
: c,
|
||||
),
|
||||
});
|
||||
}}
|
||||
onRemove={() => {
|
||||
if (
|
||||
group.conditions.length ===
|
||||
1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
updateGroup(group.id, {
|
||||
...group,
|
||||
conditions:
|
||||
group.conditions.filter(
|
||||
(c) =>
|
||||
c.id !==
|
||||
condition.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
canRemove={
|
||||
group.conditions.length > 1
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function AccountBalanceCard({
|
|||
iv={account.name_iv}
|
||||
/>
|
||||
</CardTitle>
|
||||
<div className="text-muted-foreground text-xs font-medium">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
{account.type}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
|
@ -58,7 +58,7 @@ export function AccountBalanceCard({
|
|||
<div className="text-2xl font-bold">
|
||||
{formatter.format(account.currentBalance / 100)}
|
||||
</div>
|
||||
<p className="text-muted-foreground flex items-center text-xs">
|
||||
<p className="flex items-center text-xs text-muted-foreground">
|
||||
<span
|
||||
className={
|
||||
isPositive
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ export function CashFlowCard({
|
|||
});
|
||||
|
||||
const currentNet = income - expense;
|
||||
|
||||
|
||||
const calculateTrend = () => {
|
||||
if (!previous) return undefined;
|
||||
const previousNet = previous.income - previous.expense;
|
||||
if (previousNet === 0) return undefined;
|
||||
|
||||
|
||||
const diff = currentNet - previousNet;
|
||||
const percentage = (diff / Math.abs(previousNet)) * 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ export function NetWorthChart({ data, loading }: NetWorthChartProps) {
|
|||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
axisLine={false}
|
||||
|
|
@ -100,5 +103,3 @@ export function NetWorthChart({ data, loading }: NetWorthChartProps) {
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function SpendingSummaryCard({
|
|||
if (!previous) return undefined;
|
||||
const diff = current - previous;
|
||||
const percentage = (diff / previous) * 100;
|
||||
|
||||
|
||||
return {
|
||||
value: percentage,
|
||||
label: 'from previous period',
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function StatCard({
|
|||
<CardTitle className="text-sm font-medium">
|
||||
{title}
|
||||
</CardTitle>
|
||||
{Icon && <Icon className="text-muted-foreground size-4" />}
|
||||
{Icon && <Icon className="size-4 text-muted-foreground" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-8 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700" />
|
||||
|
|
@ -45,12 +45,12 @@ export function StatCard({
|
|||
<Card className={className}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{Icon && <Icon className="text-muted-foreground size-4" />}
|
||||
{Icon && <Icon className="size-4 text-muted-foreground" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{(description || trend) && (
|
||||
<div className="text-muted-foreground flex items-center text-xs">
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
{trend && (
|
||||
<span
|
||||
className={cn(
|
||||
|
|
@ -77,5 +77,3 @@ export function StatCard({
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ interface TopCategoriesCardProps {
|
|||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function TopCategoriesCard({ categories, loading }: TopCategoriesCardProps) {
|
||||
export function TopCategoriesCard({
|
||||
categories,
|
||||
loading,
|
||||
}: TopCategoriesCardProps) {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
|
|
@ -37,7 +40,9 @@ export function TopCategoriesCard({ categories, loading }: TopCategoriesCardProp
|
|||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{categories.map((item) => {
|
||||
const Icon = (Icons[item.category.icon as keyof typeof Icons] || Icons.HelpCircle) as LucideIcon;
|
||||
const Icon = (Icons[
|
||||
item.category.icon as keyof typeof Icons
|
||||
] || Icons.HelpCircle) as LucideIcon;
|
||||
return (
|
||||
<div
|
||||
key={item.category.id}
|
||||
|
|
@ -53,10 +58,10 @@ export function TopCategoriesCard({ categories, loading }: TopCategoriesCardProp
|
|||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{item.category.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatter.format(item.amount / 100)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -64,7 +69,7 @@ export function TopCategoriesCard({ categories, loading }: TopCategoriesCardProp
|
|||
);
|
||||
})}
|
||||
{categories.length === 0 && (
|
||||
<div className="text-muted-foreground col-span-full py-8 text-center">
|
||||
<div className="col-span-full py-8 text-center text-muted-foreground">
|
||||
No spending data this month
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -73,5 +78,3 @@ export function TopCategoriesCard({ categories, loading }: TopCategoriesCardProp
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ export function EncryptedText(props: EncryptedTextProps) {
|
|||
iv: string;
|
||||
value: string;
|
||||
} | null>(null);
|
||||
const [displayState, setDisplayState] = useState<DisplayState>(() => getInitialDisplayState(isKeySet));
|
||||
const [displayState, setDisplayState] = useState<DisplayState>(() =>
|
||||
getInitialDisplayState(isKeySet),
|
||||
);
|
||||
const prevIsKeySetRef = useRef(isKeySet);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,13 @@ import {
|
|||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type Category, getCategoryColorClasses } from '@/types/category';
|
||||
import { Check, ChevronsUpDown, HelpCircle, type LucideIcon } from 'lucide-react';
|
||||
import * as Icons from 'lucide-react';
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
HelpCircle,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
const iconCache = new Map<string, LucideIcon>();
|
||||
|
|
@ -24,7 +29,9 @@ function getIconComponent(iconName?: string): LucideIcon | null {
|
|||
if (iconCache.has(iconName)) {
|
||||
return iconCache.get(iconName)!;
|
||||
}
|
||||
const icon = Icons[iconName as keyof typeof Icons] as LucideIcon | undefined;
|
||||
const icon = Icons[iconName as keyof typeof Icons] as
|
||||
| LucideIcon
|
||||
| undefined;
|
||||
if (icon) {
|
||||
iconCache.set(iconName, icon);
|
||||
}
|
||||
|
|
@ -69,7 +76,7 @@ export function CategoryCombobox({
|
|||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'max-w-full w-full justify-between !pl-2',
|
||||
'w-full max-w-full justify-between !pl-2',
|
||||
triggerClassName,
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
|
@ -77,7 +84,9 @@ export function CategoryCombobox({
|
|||
{selectedCategory ? (
|
||||
<div className="flex items-center gap-2 overflow-x-hidden">
|
||||
<CategoryIcon category={selectedCategory} />
|
||||
<span className='truncate'>{selectedCategory.name}</span>
|
||||
<span className="truncate">
|
||||
{selectedCategory.name}
|
||||
</span>
|
||||
</div>
|
||||
) : value === 'null' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -134,7 +143,9 @@ export function CategoryCombobox({
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CategoryIcon category={category} />
|
||||
<span className='truncate'>{category.name}</span>
|
||||
<span className="truncate">
|
||||
{category.name}
|
||||
</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
|
|
@ -153,7 +164,13 @@ export function CategoryCombobox({
|
|||
);
|
||||
}
|
||||
|
||||
export const CategoryIcon = memo(function CategoryIcon({ category, size = 5 }: { category: Category, size?: number }) {
|
||||
export const CategoryIcon = memo(function CategoryIcon({
|
||||
category,
|
||||
size = 5,
|
||||
}: {
|
||||
category: Category;
|
||||
size?: number;
|
||||
}) {
|
||||
const colorClasses = getCategoryColorClasses(category.color);
|
||||
const iconName = category.icon;
|
||||
|
||||
|
|
@ -165,14 +182,22 @@ export const CategoryIcon = memo(function CategoryIcon({ category, size = 5 }: {
|
|||
colorClasses.bg,
|
||||
)}
|
||||
>
|
||||
<DynamicIcon name={iconName} className={cn('h-3 w-3', colorClasses.text)} />
|
||||
<DynamicIcon
|
||||
name={iconName}
|
||||
className={cn('h-3 w-3', colorClasses.text)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DynamicIcon = memo(function DynamicIcon({ name, className }: { name?: string; className?: string }) {
|
||||
const DynamicIcon = memo(function DynamicIcon({
|
||||
name,
|
||||
className,
|
||||
}: {
|
||||
name?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const Icon = getIconComponent(name);
|
||||
if (!Icon) return null;
|
||||
return <Icon className={className} />;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -74,4 +74,3 @@ export function SyncStatusButton() {
|
|||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export function BulkActionsBar({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 w-full flex items-center justify-center animate-in duration-300 fade-in fade-out slide-in-from-bottom-5 slide-out-to-bottom-5">
|
||||
<div className="flex flex-row max-w-[75%] items-center justify-between gap-10 rounded-full border bg-background px-4 py-2 shadow-lg">
|
||||
<div className="fixed bottom-6 flex w-full animate-in items-center justify-center duration-300 slide-in-from-bottom-5 slide-out-to-bottom-5 fade-in fade-out">
|
||||
<div className="flex max-w-[75%] flex-row items-center justify-between gap-10 rounded-full border bg-background px-4 py-2 shadow-lg">
|
||||
<div className="pl-2 text-sm">
|
||||
{selectedCount} transaction{selectedCount !== 1 ? 's' : ''}{' '}
|
||||
selected
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ export function EditTransactionDialog({
|
|||
const [decryptedAccountNames, setDecryptedAccountNames] = useState<
|
||||
Map<string, string>
|
||||
>(new Map());
|
||||
const [automationRules, setAutomationRules] = useState<AutomationRule[]>([]);
|
||||
const [automationRules, setAutomationRules] = useState<AutomationRule[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && transaction) {
|
||||
|
|
@ -145,7 +147,12 @@ export function EditTransactionDialog({
|
|||
|
||||
async function checkAndApplyAutomationRules() {
|
||||
if (mode !== 'create' || automationRules.length === 0) {
|
||||
return { categoryId: null, notes: null, notesIv: null, ruleName: null };
|
||||
return {
|
||||
categoryId: null,
|
||||
notes: null,
|
||||
notesIv: null,
|
||||
ruleName: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = evaluateRulesForNewTransaction(
|
||||
|
|
@ -163,7 +170,12 @@ export function EditTransactionDialog({
|
|||
);
|
||||
|
||||
if (!result) {
|
||||
return { categoryId: null, notes: null, notesIv: null, ruleName: null };
|
||||
return {
|
||||
categoryId: null,
|
||||
notes: null,
|
||||
notesIv: null,
|
||||
ruleName: null,
|
||||
};
|
||||
}
|
||||
|
||||
let finalNotes = notes.trim();
|
||||
|
|
@ -222,7 +234,10 @@ export function EditTransactionDialog({
|
|||
toast.error('Date is required');
|
||||
return;
|
||||
}
|
||||
} else if (mode === 'edit' && transaction?.source === 'manually_created') {
|
||||
} else if (
|
||||
mode === 'edit' &&
|
||||
transaction?.source === 'manually_created'
|
||||
) {
|
||||
if (!description.trim()) {
|
||||
toast.error('Description is required');
|
||||
return;
|
||||
|
|
@ -288,8 +303,8 @@ export function EditTransactionDialog({
|
|||
|
||||
const updatedCategory = finalCategoryId
|
||||
? categories.find(
|
||||
(category) => category.id === finalCategoryId,
|
||||
) || null
|
||||
(category) => category.id === finalCategoryId,
|
||||
) || null
|
||||
: null;
|
||||
|
||||
const newTransaction: DecryptedTransaction = {
|
||||
|
|
@ -315,7 +330,8 @@ export function EditTransactionDialog({
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedCategoryId = categoryId === 'null' ? null : categoryId;
|
||||
const selectedCategoryId =
|
||||
categoryId === 'null' ? null : categoryId;
|
||||
const trimmedNotes = notes.trim();
|
||||
const trimmedDescription = description.trim();
|
||||
|
||||
|
|
@ -340,10 +356,17 @@ export function EditTransactionDialog({
|
|||
notes_iv: notesIv,
|
||||
};
|
||||
|
||||
let finalDecryptedDescription = transaction.decryptedDescription;
|
||||
let finalDecryptedDescription =
|
||||
transaction.decryptedDescription;
|
||||
|
||||
if (transaction.source === 'manually_created' && trimmedDescription) {
|
||||
const encryptedDescription = await encrypt(trimmedDescription, key);
|
||||
if (
|
||||
transaction.source === 'manually_created' &&
|
||||
trimmedDescription
|
||||
) {
|
||||
const encryptedDescription = await encrypt(
|
||||
trimmedDescription,
|
||||
key,
|
||||
);
|
||||
updateData.description = encryptedDescription.encrypted;
|
||||
updateData.description_iv = encryptedDescription.iv;
|
||||
finalDecryptedDescription = trimmedDescription;
|
||||
|
|
@ -356,8 +379,8 @@ export function EditTransactionDialog({
|
|||
);
|
||||
const updatedCategory = selectedCategoryId
|
||||
? categories.find(
|
||||
(category) => category.id === selectedCategoryId,
|
||||
) || null
|
||||
(category) => category.id === selectedCategoryId,
|
||||
) || null
|
||||
: null;
|
||||
|
||||
const updatedTransaction: DecryptedTransaction = {
|
||||
|
|
@ -365,8 +388,10 @@ export function EditTransactionDialog({
|
|||
category_id: selectedCategoryId,
|
||||
category: updatedCategory,
|
||||
decryptedDescription: finalDecryptedDescription,
|
||||
description: updateData.description ?? transaction.description,
|
||||
description_iv: updateData.description_iv ?? transaction.description_iv,
|
||||
description:
|
||||
updateData.description ?? transaction.description,
|
||||
description_iv:
|
||||
updateData.description_iv ?? transaction.description_iv,
|
||||
decryptedNotes: trimmedNotes || null,
|
||||
notes: encryptedNotes,
|
||||
notes_iv: notesIv,
|
||||
|
|
@ -388,9 +413,7 @@ export function EditTransactionDialog({
|
|||
}
|
||||
}
|
||||
|
||||
const selectedAccount = accounts.find(
|
||||
(acc) => acc.id === accountId,
|
||||
);
|
||||
const selectedAccount = accounts.find((acc) => acc.id === accountId);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
|
@ -458,14 +481,17 @@ export function EditTransactionDialog({
|
|||
<Label
|
||||
htmlFor="description"
|
||||
className={
|
||||
mode === 'edit' && transaction?.source === 'imported'
|
||||
mode === 'edit' &&
|
||||
transaction?.source === 'imported'
|
||||
? 'text-sm text-muted-foreground'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
Description
|
||||
</Label>
|
||||
{mode === 'create' || (mode === 'edit' && transaction?.source === 'manually_created') ? (
|
||||
{mode === 'create' ||
|
||||
(mode === 'edit' &&
|
||||
transaction?.source === 'manually_created') ? (
|
||||
<Textarea
|
||||
id="description"
|
||||
value={description}
|
||||
|
|
@ -481,13 +507,18 @@ export function EditTransactionDialog({
|
|||
<div className="space-y-1.5">
|
||||
<Textarea
|
||||
id="description"
|
||||
value={transaction?.decryptedDescription ?? ''}
|
||||
value={
|
||||
transaction?.decryptedDescription ??
|
||||
''
|
||||
}
|
||||
disabled
|
||||
className="bg-muted"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This transaction was imported from a file. The description cannot be modified.
|
||||
This transaction was imported from a
|
||||
file. The description cannot be
|
||||
modified.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -592,8 +623,8 @@ export function EditTransactionDialog({
|
|||
{isSubmitting
|
||||
? 'Saving...'
|
||||
: mode === 'create'
|
||||
? 'Create Transaction'
|
||||
: 'Save Changes'}
|
||||
? 'Create Transaction'
|
||||
: 'Save Changes'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ interface ImportStepMappingProps {
|
|||
dateFormatDetected: boolean;
|
||||
parsedData: ParsedRow[];
|
||||
currencyCode: string;
|
||||
onMappingChange: (field: keyof ColumnMapping, value: string | string[]) => void;
|
||||
onMappingChange: (
|
||||
field: keyof ColumnMapping,
|
||||
value: string | string[],
|
||||
) => void;
|
||||
onDateFormatChange: (format: DateFormat) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
|
|
@ -69,7 +72,7 @@ export function ImportStepMapping({
|
|||
|
||||
const handleDescriptionChange = (index: number, value: string) => {
|
||||
const newColumns = [...descriptionColumns];
|
||||
|
||||
|
||||
if (value === '__none__') {
|
||||
newColumns.splice(index, 1);
|
||||
} else {
|
||||
|
|
@ -166,7 +169,9 @@ export function ImportStepMapping({
|
|||
{descriptionColumns.length === 0 ? (
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => handleDescriptionChange(0, value)}
|
||||
onValueChange={(value) =>
|
||||
handleDescriptionChange(0, value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="description-column-0">
|
||||
<SelectValue placeholder="Select description column" />
|
||||
|
|
@ -194,7 +199,10 @@ export function ImportStepMapping({
|
|||
<Select
|
||||
value={column || ''}
|
||||
onValueChange={(value) =>
|
||||
handleDescriptionChange(columnIndex, value)
|
||||
handleDescriptionChange(
|
||||
columnIndex,
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
|
|
@ -214,23 +222,29 @@ export function ImportStepMapping({
|
|||
None (Remove)
|
||||
</SelectItem>
|
||||
)}
|
||||
{columnOptions.map((option, index) => (
|
||||
<SelectItem
|
||||
key={`desc-${columnIndex}-${option.value}-${index}`}
|
||||
value={option.value}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{columnOptions.map(
|
||||
(option, index) => (
|
||||
<SelectItem
|
||||
key={`desc-${columnIndex}-${option.value}-${index}`}
|
||||
value={option.value}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
{descriptionColumns.length > 0 &&
|
||||
descriptionColumns.length < 3 &&
|
||||
descriptionColumns[descriptionColumns.length - 1] !== '' && (
|
||||
descriptionColumns[
|
||||
descriptionColumns.length - 1
|
||||
] !== '' && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -272,13 +286,14 @@ export function ImportStepMapping({
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="balance-column">
|
||||
Balance (Optional)
|
||||
</Label>
|
||||
<Label htmlFor="balance-column">Balance (Optional)</Label>
|
||||
<Select
|
||||
value={columnMapping.balance || '__none__'}
|
||||
onValueChange={(value) =>
|
||||
onMappingChange('balance', value === '__none__' ? '' : value)
|
||||
onMappingChange(
|
||||
'balance',
|
||||
value === '__none__' ? '' : value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="balance-column">
|
||||
|
|
@ -307,10 +322,10 @@ export function ImportStepMapping({
|
|||
{previewTransactions.map((transaction, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start justify-between rounded-md bg-background p-3 text-sm gap-3"
|
||||
className="flex items-start justify-between gap-3 rounded-md bg-background p-3 text-sm"
|
||||
>
|
||||
<div className="flex flex-1 items-start gap-3">
|
||||
<span className="text-muted-foreground whitespace-nowrap">
|
||||
<span className="whitespace-nowrap text-muted-foreground">
|
||||
{transaction.date}
|
||||
</span>
|
||||
<span className="flex-1 whitespace-pre-line">
|
||||
|
|
|
|||
|
|
@ -6,24 +6,12 @@ import {
|
|||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useEncryptionKey } from '@/contexts/encryption-key-context';
|
||||
import { type Account, type Bank } from '@/types/account';
|
||||
import { type Category } from '@/types/category';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ImportTransactionsDrawer } from './import-transactions-drawer';
|
||||
|
||||
interface ImportTransactionsButtonProps {
|
||||
categories: Category[];
|
||||
accounts: Account[];
|
||||
banks: Bank[];
|
||||
}
|
||||
|
||||
export function ImportTransactionsButton({
|
||||
categories,
|
||||
accounts,
|
||||
banks,
|
||||
}: ImportTransactionsButtonProps) {
|
||||
export function ImportTransactionsButton() {
|
||||
const { isKeySet } = useEncryptionKey();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
|
|
@ -63,9 +51,6 @@ export function ImportTransactionsButton({
|
|||
<ImportTransactionsDrawer
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
categories={categories}
|
||||
accounts={accounts}
|
||||
banks={banks}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@/components/ui/drawer';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { useEncryptionKey } from '@/contexts/encryption-key-context';
|
||||
import { importKey } from '@/lib/crypto';
|
||||
import {
|
||||
|
|
@ -32,7 +33,6 @@ import {
|
|||
type ColumnMapping,
|
||||
type ImportState,
|
||||
} from '@/types/import';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -44,9 +44,6 @@ import { ImportStepUpload } from './import-step-upload';
|
|||
interface ImportTransactionsDrawerProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
categories: import('@/types/category').Category[];
|
||||
accounts: import('@/types/account').Account[];
|
||||
banks: import('@/types/account').Bank[];
|
||||
}
|
||||
|
||||
interface ImportError {
|
||||
|
|
@ -62,9 +59,6 @@ interface ImportError {
|
|||
export function ImportTransactionsDrawer({
|
||||
open,
|
||||
onOpenChange,
|
||||
categories,
|
||||
accounts,
|
||||
banks,
|
||||
}: ImportTransactionsDrawerProps) {
|
||||
const { isKeySet } = useEncryptionKey();
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
|
|
@ -400,7 +394,11 @@ export function ImportTransactionsDrawer({
|
|||
const createdTransaction =
|
||||
await transactionSyncService.create(transactionData);
|
||||
|
||||
return { success: true, transaction: createdTransaction, rowNumber };
|
||||
return {
|
||||
success: true,
|
||||
transaction: createdTransaction,
|
||||
rowNumber,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -415,14 +413,17 @@ export function ImportTransactionsDrawer({
|
|||
result.reason instanceof Error
|
||||
? result.reason.message
|
||||
: 'Unknown error';
|
||||
|
||||
|
||||
console.error(`Transaction ${rowNumber} failed:`, {
|
||||
transaction,
|
||||
error: result.reason,
|
||||
errorMessage,
|
||||
stack: result.reason instanceof Error ? result.reason.stack : undefined,
|
||||
stack:
|
||||
result.reason instanceof Error
|
||||
? result.reason.stack
|
||||
: undefined,
|
||||
});
|
||||
|
||||
|
||||
errors.push({
|
||||
rowNumber,
|
||||
transaction: {
|
||||
|
|
@ -441,20 +442,26 @@ export function ImportTransactionsDrawer({
|
|||
|
||||
const balancesToImport = new Map<string, number>();
|
||||
for (const transaction of newTransactions) {
|
||||
if (transaction.balance !== null && transaction.balance !== undefined) {
|
||||
balancesToImport.set(transaction.transaction_date, transaction.balance);
|
||||
if (
|
||||
transaction.balance !== null &&
|
||||
transaction.balance !== undefined
|
||||
) {
|
||||
balancesToImport.set(
|
||||
transaction.transaction_date,
|
||||
transaction.balance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (balancesToImport.size > 0) {
|
||||
try {
|
||||
const balanceRecords = Array.from(balancesToImport.entries()).map(
|
||||
([date, balance]) => ({
|
||||
account_id: selectedAccount.id,
|
||||
balance_date: date,
|
||||
balance,
|
||||
}),
|
||||
);
|
||||
const balanceRecords = Array.from(
|
||||
balancesToImport.entries(),
|
||||
).map(([date, balance]) => ({
|
||||
account_id: selectedAccount.id,
|
||||
balance_date: date,
|
||||
balance,
|
||||
}));
|
||||
|
||||
await accountBalanceSyncService.createMany(balanceRecords);
|
||||
} catch (err) {
|
||||
|
|
@ -495,7 +502,10 @@ export function ImportTransactionsDrawer({
|
|||
}
|
||||
|
||||
transactionSyncService.sync().catch((syncError) => {
|
||||
console.error('Failed to sync transactions with backend:', syncError);
|
||||
console.error(
|
||||
'Failed to sync transactions with backend:',
|
||||
syncError,
|
||||
);
|
||||
});
|
||||
|
||||
accountBalanceSyncService.sync().catch((syncError) => {
|
||||
|
|
@ -536,7 +546,8 @@ export function ImportTransactionsDrawer({
|
|||
if (isImporting) {
|
||||
return {
|
||||
title: 'Importing Transactions',
|
||||
description: 'Please wait while we import your transactions',
|
||||
description:
|
||||
'Please wait while we import your transactions',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -597,14 +608,16 @@ export function ImportTransactionsDrawer({
|
|||
};
|
||||
|
||||
const renderImportProgress = () => {
|
||||
const percentage = importTotal > 0 ? (importProgress / importTotal) * 100 : 0;
|
||||
const percentage =
|
||||
importTotal > 0 ? (importProgress / importTotal) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{importProgress} of {importTotal} transactions imported
|
||||
{importProgress} of {importTotal} transactions
|
||||
imported
|
||||
</span>
|
||||
<span>{Math.round(percentage)}%</span>
|
||||
</div>
|
||||
|
|
@ -648,13 +661,13 @@ export function ImportTransactionsDrawer({
|
|||
<td className="px-4 py-2">
|
||||
{error.transaction.date}
|
||||
</td>
|
||||
<td className="px-4 py-2 max-w-[200px] truncate">
|
||||
<td className="max-w-[200px] truncate px-4 py-2">
|
||||
{error.transaction.description}
|
||||
</td>
|
||||
<td className="px-4 py-2 font-mono">
|
||||
{error.transaction.amount}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-destructive max-w-[200px] truncate">
|
||||
<td className="max-w-[200px] truncate px-4 py-2 text-destructive">
|
||||
{error.error}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export function createTransactionColumns({
|
|||
cell: ({ row }) => {
|
||||
const bank = row.original.bank;
|
||||
return (
|
||||
<div className="flex px-1 items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
{bank?.logo && (
|
||||
<img
|
||||
src={bank.logo}
|
||||
|
|
@ -154,7 +154,7 @@ export function createTransactionColumns({
|
|||
cell: ({ row }) => {
|
||||
const account = row.original.account;
|
||||
if (!account) {
|
||||
return <div className='px-1'>N/A</div>;
|
||||
return <div className="px-1">N/A</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -162,7 +162,7 @@ export function createTransactionColumns({
|
|||
encryptedText={account.name}
|
||||
iv={account.name_iv}
|
||||
length={{ min: 5, max: 10 }}
|
||||
className="max-w-[100px] px-1 truncate"
|
||||
className="max-w-[100px] truncate px-1"
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -185,7 +185,7 @@ export function createTransactionColumns({
|
|||
}).format(amount);
|
||||
|
||||
return (
|
||||
<div className={`text-right pl-4`}>
|
||||
<div className={`pl-4 text-right`}>
|
||||
<span
|
||||
className={`${amount < 0 ? '' : 'bg-green-100/70 dark:bg-green-900'}`}
|
||||
>
|
||||
|
|
@ -202,14 +202,20 @@ export function createTransactionColumns({
|
|||
maxSize: 35,
|
||||
minSize: 35,
|
||||
meta: {
|
||||
cellClassName: '!w-[35px] !max-w-[35px] !min-w-[35px] !p-0 whitespace-normal',
|
||||
cellStyle: { width: '35px', maxWidth: '35px', minWidth: '35px', padding: 0 }
|
||||
cellClassName:
|
||||
'!w-[35px] !max-w-[35px] !min-w-[35px] !p-0 whitespace-normal',
|
||||
cellStyle: {
|
||||
width: '35px',
|
||||
maxWidth: '35px',
|
||||
minWidth: '35px',
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const transaction = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-[35px]">
|
||||
<div className="flex w-[35px] items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
|
|
@ -225,7 +231,9 @@ export function createTransactionColumns({
|
|||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onReEvaluateRules(transaction)}
|
||||
onClick={() =>
|
||||
onReEvaluateRules(transaction)
|
||||
}
|
||||
>
|
||||
Re-evaluate rules
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { format } from 'date-fns';
|
||||
import { Check, ChevronsUpDown, CircleHelp, X } from 'lucide-react';
|
||||
import * as Icons from 'lucide-react';
|
||||
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import { EncryptedText } from '@/components/encrypted-text';
|
||||
|
|
@ -119,7 +119,10 @@ export function TransactionFilters({
|
|||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-96 max-h-[600px] overflow-y-auto" align="start">
|
||||
<PopoverContent
|
||||
className="max-h-[600px] w-96 overflow-y-auto"
|
||||
align="start"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium">Filters</h4>
|
||||
|
|
@ -133,9 +136,9 @@ export function TransactionFilters({
|
|||
value={
|
||||
filters.dateFrom
|
||||
? format(
|
||||
filters.dateFrom,
|
||||
'yyyy-MM-dd',
|
||||
)
|
||||
filters.dateFrom,
|
||||
'yyyy-MM-dd',
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={(e) =>
|
||||
|
|
@ -153,9 +156,9 @@ export function TransactionFilters({
|
|||
value={
|
||||
filters.dateTo
|
||||
? format(
|
||||
filters.dateTo,
|
||||
'yyyy-MM-dd',
|
||||
)
|
||||
filters.dateTo,
|
||||
'yyyy-MM-dd',
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={(e) =>
|
||||
|
|
@ -218,9 +221,13 @@ export function TransactionFilters({
|
|||
role="combobox"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{filters.categoryIds.length > 0 ? (
|
||||
{filters.categoryIds.length >
|
||||
0 ? (
|
||||
<span className="truncate">
|
||||
{filters.categoryIds.length}{' '}
|
||||
{
|
||||
filters.categoryIds
|
||||
.length
|
||||
}{' '}
|
||||
selected
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -231,7 +238,10 @@ export function TransactionFilters({
|
|||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-full p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search categories..." />
|
||||
<CommandList>
|
||||
|
|
@ -247,7 +257,7 @@ export function TransactionFilters({
|
|||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-1 flex p-1 size-4 items-center justify-center rounded-sm border border-primary',
|
||||
'mr-1 flex size-4 items-center justify-center rounded-sm border border-primary p-1',
|
||||
isUncategorizedSelected
|
||||
? 'bg-primary/10 text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
|
|
@ -262,50 +272,63 @@ export function TransactionFilters({
|
|||
Uncategorized
|
||||
</div>
|
||||
</CommandItem>
|
||||
{categories.map((category) => {
|
||||
const isSelected =
|
||||
filters.categoryIds.includes(
|
||||
category.id,
|
||||
);
|
||||
const colorClasses =
|
||||
getCategoryColorClasses(
|
||||
category.color,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={category.id}
|
||||
onSelect={() =>
|
||||
handleCategoryToggle(
|
||||
category.id,
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-1 flex p-1 size-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary/10 text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)}
|
||||
{categories.map(
|
||||
(category) => {
|
||||
const isSelected =
|
||||
filters.categoryIds.includes(
|
||||
category.id,
|
||||
);
|
||||
const colorClasses =
|
||||
getCategoryColorClasses(
|
||||
category.color,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={
|
||||
category.id
|
||||
}
|
||||
onSelect={() =>
|
||||
handleCategoryToggle(
|
||||
category.id,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-full',
|
||||
colorClasses.bg,
|
||||
'mr-1 flex size-4 items-center justify-center rounded-sm border border-primary p-1',
|
||||
isSelected
|
||||
? 'bg-primary/10 text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)}
|
||||
>
|
||||
<CategoryIcon category={category} size={4} />
|
||||
<Check className="size-3" />
|
||||
</div>
|
||||
<span>
|
||||
{category.name}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-full',
|
||||
colorClasses.bg,
|
||||
)}
|
||||
>
|
||||
<CategoryIcon
|
||||
category={
|
||||
category
|
||||
}
|
||||
size={
|
||||
4
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
{
|
||||
category.name
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
|
@ -372,12 +395,3 @@ export function TransactionFilters({
|
|||
}
|
||||
|
||||
const UNCATEGORIZED_CATEGORY_ID = -1;
|
||||
|
||||
function getIconComponent(iconName?: string): Icons.LucideIcon | null {
|
||||
if (!iconName) return null;
|
||||
const icon = Icons[iconName as keyof typeof Icons] as Icons.LucideIcon | undefined;
|
||||
if (icon && typeof icon === 'function') {
|
||||
return icon;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ function formatErrorMessage(error: string): string {
|
|||
if (error.includes('status code 5')) {
|
||||
return 'Server is temporarily unavailable. Please try again later.';
|
||||
}
|
||||
if (error.includes('status code 401') || error.includes('status code 403')) {
|
||||
if (
|
||||
error.includes('status code 401') ||
|
||||
error.includes('status code 403')
|
||||
) {
|
||||
return 'Your session has expired. Please refresh the page.';
|
||||
}
|
||||
if (error.includes('status code 4')) {
|
||||
|
|
@ -103,7 +106,9 @@ export function SyncProvider({
|
|||
}
|
||||
|
||||
if (!isOnline) {
|
||||
setError('Unable to sync while offline. Connect to the internet and try again.');
|
||||
setError(
|
||||
'Unable to sync while offline. Connect to the internet and try again.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ export function useAppearance() {
|
|||
'appearance',
|
||||
) as Appearance | null;
|
||||
|
||||
|
||||
updateAppearance(savedAppearance || 'system');
|
||||
|
||||
return () =>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Account } from '@/types/account';
|
||||
import { Category } from '@/types/category';
|
||||
import { endOfMonth, format, startOfMonth, subDays, subMonths } from 'date-fns';
|
||||
import { format, subDays, subMonths } from 'date-fns';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface DashboardData {
|
||||
|
|
@ -37,14 +37,15 @@ export interface DashboardData {
|
|||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function useDashboardData(
|
||||
categories: Category[],
|
||||
accounts: Account[],
|
||||
): DashboardData {
|
||||
export function useDashboardData(): DashboardData {
|
||||
const [data, setData] = useState<Omit<DashboardData, 'isLoading'>>({
|
||||
netWorth: { current: 0, previous: 0, diff: 0 },
|
||||
monthlySpending: { current: 0, limit: null, previous: 0 },
|
||||
cashFlow: { income: 0, expense: 0, previous: { income: 0, expense: 0 } },
|
||||
cashFlow: {
|
||||
income: 0,
|
||||
expense: 0,
|
||||
previous: { income: 0, expense: 0 },
|
||||
},
|
||||
netWorthHistory: [],
|
||||
accounts: [],
|
||||
topCategories: [],
|
||||
|
|
@ -57,15 +58,21 @@ export function useDashboardData(
|
|||
try {
|
||||
const now = new Date();
|
||||
const to = format(now, 'yyyy-MM-dd');
|
||||
|
||||
|
||||
// Last 12 months for evolution charts
|
||||
const from12Months = format(subMonths(now, 12), 'yyyy-MM-dd');
|
||||
const params12Months = new URLSearchParams({ from: from12Months, to });
|
||||
const params12Months = new URLSearchParams({
|
||||
from: from12Months,
|
||||
to,
|
||||
});
|
||||
const query12Months = `?${params12Months.toString()}`;
|
||||
|
||||
// Last 30 days for top blocks and categories
|
||||
const from30Days = format(subDays(now, 30), 'yyyy-MM-dd');
|
||||
const params30Days = new URLSearchParams({ from: from30Days, to });
|
||||
const params30Days = new URLSearchParams({
|
||||
from: from30Days,
|
||||
to,
|
||||
});
|
||||
const query30Days = `?${params30Days.toString()}`;
|
||||
|
||||
const [
|
||||
|
|
@ -74,14 +81,26 @@ export function useDashboardData(
|
|||
cashFlow,
|
||||
netWorthEvolution,
|
||||
accountBalances,
|
||||
topCategories
|
||||
topCategories,
|
||||
] = await Promise.all([
|
||||
fetch(`/api/dashboard/net-worth${query30Days}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/monthly-spending${query30Days}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/cash-flow${query30Days}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/net-worth-evolution${query12Months}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/account-balances${query12Months}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/top-categories${query30Days}`).then(r => r.json()),
|
||||
fetch(`/api/dashboard/net-worth${query30Days}`).then((r) =>
|
||||
r.json(),
|
||||
),
|
||||
fetch(`/api/dashboard/monthly-spending${query30Days}`).then(
|
||||
(r) => r.json(),
|
||||
),
|
||||
fetch(`/api/dashboard/cash-flow${query30Days}`).then((r) =>
|
||||
r.json(),
|
||||
),
|
||||
fetch(
|
||||
`/api/dashboard/net-worth-evolution${query12Months}`,
|
||||
).then((r) => r.json()),
|
||||
fetch(
|
||||
`/api/dashboard/account-balances${query12Months}`,
|
||||
).then((r) => r.json()),
|
||||
fetch(`/api/dashboard/top-categories${query30Days}`).then(
|
||||
(r) => r.json(),
|
||||
),
|
||||
]);
|
||||
|
||||
setData({
|
||||
|
|
@ -101,12 +120,19 @@ export function useDashboardData(
|
|||
previous: cashFlow.previous,
|
||||
},
|
||||
netWorthHistory: netWorthEvolution,
|
||||
accounts: accountBalances.map((acc: any) => ({
|
||||
...acc,
|
||||
currentBalance: acc.current_balance,
|
||||
previousBalance: acc.previous_balance,
|
||||
diff: acc.current_balance - acc.previous_balance,
|
||||
})),
|
||||
accounts: accountBalances.map(
|
||||
(acc: {
|
||||
id: string;
|
||||
name: string;
|
||||
current_balance: number;
|
||||
previous_balance: number;
|
||||
}) => ({
|
||||
...acc,
|
||||
currentBalance: acc.current_balance,
|
||||
previousBalance: acc.previous_balance,
|
||||
diff: acc.current_balance - acc.previous_balance,
|
||||
}),
|
||||
),
|
||||
topCategories,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -21,4 +21,3 @@ export function useOnlineStatus() {
|
|||
|
||||
return isOnline;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,12 +107,13 @@ export function useReEvaluateAllTransactions() {
|
|||
}
|
||||
|
||||
toast.dismiss(toastId);
|
||||
toast.success(
|
||||
() => (<div>
|
||||
{`Re-evaluation complete!`}<br />
|
||||
toast.success(() => (
|
||||
<div>
|
||||
{`Re-evaluation complete!`}
|
||||
<br />
|
||||
{`${successCount} transaction(s) updated.`}
|
||||
</div >)
|
||||
);
|
||||
</div>
|
||||
));
|
||||
} catch (error) {
|
||||
console.error('Failed to re-evaluate transactions:', error);
|
||||
toast.error(
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export const LEAD_FUNNEL_EVENT_UUID = '9668a06c-dee9-47a8-9bee-eaaa2a3a5915';
|
||||
|
||||
|
|
|
|||
|
|
@ -309,7 +309,10 @@ export function autoDetectColumns(headers: string[]): ColumnMapping {
|
|||
mapping.amount = originalHeader;
|
||||
}
|
||||
|
||||
if (!mapping.balance && balancePatterns.some((p) => header.includes(p))) {
|
||||
if (
|
||||
!mapping.balance &&
|
||||
balancePatterns.some((p) => header.includes(p))
|
||||
) {
|
||||
mapping.balance = originalHeader;
|
||||
}
|
||||
}
|
||||
|
|
@ -527,7 +530,9 @@ export function convertRowsToTransactions(
|
|||
|
||||
let balance: number | null = null;
|
||||
if (mapping.balance && row[mapping.balance]) {
|
||||
const parsedBalance = parseAmount(row[mapping.balance] as string | number);
|
||||
const parsedBalance = parseAmount(
|
||||
row[mapping.balance] as string | number,
|
||||
);
|
||||
if (parsedBalance !== null) {
|
||||
balance = Math.round(parsedBalance * 100);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,13 +75,17 @@ export function prepareTransactionData(
|
|||
};
|
||||
|
||||
return {
|
||||
description: normalizeWhitespace((transaction.decryptedDescription || '').toLowerCase()),
|
||||
description: normalizeWhitespace(
|
||||
(transaction.decryptedDescription || '').toLowerCase(),
|
||||
),
|
||||
amount: transaction.amount / 100,
|
||||
transaction_date: transaction.transaction_date,
|
||||
bank_name: bank?.name || '',
|
||||
account_name: account?.name || '',
|
||||
category: category?.name || null,
|
||||
notes: transaction.decryptedNotes ? normalizeWhitespace(transaction.decryptedNotes.toLowerCase()) : null,
|
||||
notes: transaction.decryptedNotes
|
||||
? normalizeWhitespace(transaction.decryptedNotes.toLowerCase())
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -188,16 +192,19 @@ export function evaluateRulesForNewTransaction(
|
|||
banks: Bank[],
|
||||
): RuleEvaluationResult | null {
|
||||
if (!rules || !categories || !accounts || !banks) {
|
||||
consoleDebug('[Rule Engine] Missing required data for rule evaluation', {
|
||||
hasRules: !!rules,
|
||||
rulesLength: rules?.length,
|
||||
hasCategories: !!categories,
|
||||
categoriesLength: categories?.length,
|
||||
hasAccounts: !!accounts,
|
||||
accountsLength: accounts?.length,
|
||||
hasBanks: !!banks,
|
||||
banksLength: banks?.length,
|
||||
});
|
||||
consoleDebug(
|
||||
'[Rule Engine] Missing required data for rule evaluation',
|
||||
{
|
||||
hasRules: !!rules,
|
||||
rulesLength: rules?.length,
|
||||
hasCategories: !!categories,
|
||||
categoriesLength: categories?.length,
|
||||
hasAccounts: !!accounts,
|
||||
accountsLength: accounts?.length,
|
||||
hasBanks: !!banks,
|
||||
banksLength: banks?.length,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -213,16 +220,23 @@ export function evaluateRulesForNewTransaction(
|
|||
};
|
||||
|
||||
const preparedData: TransactionData = {
|
||||
description: normalizeWhitespace(transactionData.description.toLowerCase()),
|
||||
description: normalizeWhitespace(
|
||||
transactionData.description.toLowerCase(),
|
||||
),
|
||||
amount: transactionData.amount,
|
||||
transaction_date: transactionData.transaction_date,
|
||||
bank_name: bank?.name || '',
|
||||
account_name: account?.name || '',
|
||||
category: null,
|
||||
notes: transactionData.notes ? normalizeWhitespace(transactionData.notes.toLowerCase()) : null,
|
||||
notes: transactionData.notes
|
||||
? normalizeWhitespace(transactionData.notes.toLowerCase())
|
||||
: null,
|
||||
};
|
||||
|
||||
consoleDebug('[Rule Engine] Evaluating new transaction data:', preparedData);
|
||||
consoleDebug(
|
||||
'[Rule Engine] Evaluating new transaction data:',
|
||||
preparedData,
|
||||
);
|
||||
consoleDebug(`[Rule Engine] Evaluating ${sortedRules.length} rules`);
|
||||
|
||||
for (const rule of sortedRules) {
|
||||
|
|
@ -238,10 +252,7 @@ export function evaluateRulesForNewTransaction(
|
|||
normalizedRulesJson,
|
||||
);
|
||||
|
||||
const result = jsonLogic.apply(
|
||||
normalizedRulesJson,
|
||||
preparedData,
|
||||
);
|
||||
const result = jsonLogic.apply(normalizedRulesJson, preparedData);
|
||||
|
||||
consoleDebug(`[Rule Engine] Rule #${rule.id} result:`, result);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import type { UUID } from '@/types/uuid';
|
||||
import axios from 'axios';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
import { db } from './dexie-db';
|
||||
|
||||
|
|
@ -21,8 +21,12 @@ export interface IndexedDBRecord {
|
|||
export interface SyncOptions {
|
||||
storeName: StoreName;
|
||||
endpoint: string;
|
||||
transformFromServer?: (data: Record<string, unknown>) => Record<string, unknown>;
|
||||
transformToServer?: (data: Record<string, unknown>) => Record<string, unknown>;
|
||||
transformFromServer?: (
|
||||
data: Record<string, unknown>,
|
||||
) => Record<string, unknown>;
|
||||
transformToServer?: (
|
||||
data: Record<string, unknown>,
|
||||
) => Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
|
|
@ -114,7 +118,12 @@ export class SyncManager {
|
|||
|
||||
const table = db[this.options.storeName];
|
||||
const localRecords = await table.toArray();
|
||||
const localMap = new Map(localRecords.map((r) => [(r as IndexedDBRecord).id, r as IndexedDBRecord]));
|
||||
const localMap = new Map(
|
||||
localRecords.map((r) => [
|
||||
(r as IndexedDBRecord).id,
|
||||
r as IndexedDBRecord,
|
||||
]),
|
||||
);
|
||||
|
||||
const toInsert: Record<string, unknown>[] = [];
|
||||
const toUpdate: Record<string, unknown>[] = [];
|
||||
|
|
@ -268,4 +277,3 @@ export class SyncManager {
|
|||
return this.syncInProgress;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { usePage } from '@inertiajs/react';
|
||||
import type { SharedData } from '@/types';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
|
||||
export interface TrackEventPayload {
|
||||
step: string;
|
||||
|
|
@ -59,7 +59,11 @@ export async function trackEvent(
|
|||
export function useTrackEvent() {
|
||||
const { auth } = usePage<SharedData>().props;
|
||||
|
||||
return (eventUuid: string, payload: Omit<TrackEventPayload, 'user_id'>, options?: TrackEventOptions) => {
|
||||
return (
|
||||
eventUuid: string,
|
||||
payload: Omit<TrackEventPayload, 'user_id'>,
|
||||
options?: TrackEventOptions,
|
||||
) => {
|
||||
const finalPayload: TrackEventPayload = {
|
||||
...payload,
|
||||
user_id: auth?.user?.id,
|
||||
|
|
|
|||
|
|
@ -32,11 +32,16 @@ function isCryptoAvailable(): boolean {
|
|||
return !!(window.crypto && window.crypto.subtle);
|
||||
}
|
||||
|
||||
function getInitialErrors(): { password?: string; confirmPassword?: string; general?: string } {
|
||||
function getInitialErrors(): {
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
general?: string;
|
||||
} {
|
||||
if (typeof window === 'undefined') return {};
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
return {
|
||||
general: 'Web Crypto API is not available. Please ensure you are accessing this page via HTTPS or localhost.',
|
||||
general:
|
||||
'Web Crypto API is not available. Please ensure you are accessing this page via HTTPS or localhost.',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -9,16 +9,8 @@ import { useDashboardData } from '@/hooks/use-dashboard-data';
|
|||
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
|
||||
import { dashboard } from '@/routes';
|
||||
import { BreadcrumbItem } from '@/types';
|
||||
import { Account, Bank } from '@/types/account';
|
||||
import { Category } from '@/types/category';
|
||||
import { Head } from '@inertiajs/react';
|
||||
|
||||
interface Props {
|
||||
categories: Category[];
|
||||
accounts: Account[];
|
||||
banks: Bank[];
|
||||
}
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
|
|
@ -26,7 +18,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||
},
|
||||
];
|
||||
|
||||
export default function Dashboard({ categories, accounts }: Props) {
|
||||
export default function Dashboard() {
|
||||
const {
|
||||
netWorth,
|
||||
monthlySpending,
|
||||
|
|
@ -35,7 +27,7 @@ export default function Dashboard({ categories, accounts }: Props) {
|
|||
accounts: accountMetrics,
|
||||
topCategories,
|
||||
isLoading,
|
||||
} = useDashboardData(categories, accounts);
|
||||
} = useDashboardData();
|
||||
|
||||
return (
|
||||
<AppSidebarLayout breadcrumbs={breadcrumbs}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
import { type SharedData } from '@/types';
|
||||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
|
||||
export default function Privacy() {
|
||||
const { appUrl } = usePage<SharedData>().props;
|
||||
|
|
@ -14,7 +14,10 @@ export default function Privacy() {
|
|||
<link rel="canonical" href={`${appUrl}/privacy`} />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<meta property="og:title" content="Privacy Policy - Whisper Money" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Privacy Policy - Whisper Money"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Privacy policy for Whisper Money. Learn how we collect, use, and protect your personal information."
|
||||
|
|
@ -35,7 +38,7 @@ export default function Privacy() {
|
|||
Privacy Policy
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-neutral max-w-none dark:prose-invert">
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none">
|
||||
<p className="text-lg text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</p>
|
||||
|
|
@ -45,12 +48,20 @@ export default function Privacy() {
|
|||
1. Data Controller
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Whisper Money is the data controller responsible for your personal information.
|
||||
Whisper Money is the data controller responsible
|
||||
for your personal information.
|
||||
</p>
|
||||
<div className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<p><strong>Company Name:</strong> Whisper Money</p>
|
||||
<p><strong>Address:</strong> Calle Oca, Madrid - 28025, Spain</p>
|
||||
<p><strong>Email:</strong> victor@whisper.money</p>
|
||||
<p>
|
||||
<strong>Company Name:</strong> Whisper Money
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> Calle Oca, Madrid
|
||||
- 28025, Spain
|
||||
</p>
|
||||
<p>
|
||||
<strong>Email:</strong> victor@whisper.money
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -59,13 +70,30 @@ export default function Privacy() {
|
|||
2. Information We Collect
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We collect the following types of personal information:
|
||||
We collect the following types of personal
|
||||
information:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li><strong>Account Information:</strong> Email address, name, and password (encrypted)</li>
|
||||
<li><strong>Financial Data:</strong> Transaction details, budgets, categories, and other financial information you manually enter into the application</li>
|
||||
<li><strong>Usage Information:</strong> Information about how you use our service, including access times and features used</li>
|
||||
<li><strong>Technical Information:</strong> IP address, browser type, device information, and operating system</li>
|
||||
<li>
|
||||
<strong>Account Information:</strong> Email
|
||||
address, name, and password (encrypted)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Financial Data:</strong> Transaction
|
||||
details, budgets, categories, and other
|
||||
financial information you manually enter
|
||||
into the application
|
||||
</li>
|
||||
<li>
|
||||
<strong>Usage Information:</strong>{' '}
|
||||
Information about how you use our service,
|
||||
including access times and features used
|
||||
</li>
|
||||
<li>
|
||||
<strong>Technical Information:</strong> IP
|
||||
address, browser type, device information,
|
||||
and operating system
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
|
@ -74,20 +102,45 @@ export default function Privacy() {
|
|||
3. How We Use Your Information
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We process your personal data for the following purposes:
|
||||
We process your personal data for the following
|
||||
purposes:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li>To provide and maintain our personal finance tracking service</li>
|
||||
<li>To enable cloud synchronization of your encrypted financial data across devices</li>
|
||||
<li>To authenticate your access and protect your account security</li>
|
||||
<li>To process payments for premium features or subscriptions</li>
|
||||
<li>To send you service-related notifications, updates, and security alerts via email</li>
|
||||
<li>To improve and optimize our service based on usage patterns</li>
|
||||
<li>To comply with legal obligations and enforce our terms</li>
|
||||
<li>
|
||||
To provide and maintain our personal finance
|
||||
tracking service
|
||||
</li>
|
||||
<li>
|
||||
To enable cloud synchronization of your
|
||||
encrypted financial data across devices
|
||||
</li>
|
||||
<li>
|
||||
To authenticate your access and protect your
|
||||
account security
|
||||
</li>
|
||||
<li>
|
||||
To process payments for premium features or
|
||||
subscriptions
|
||||
</li>
|
||||
<li>
|
||||
To send you service-related notifications,
|
||||
updates, and security alerts via email
|
||||
</li>
|
||||
<li>
|
||||
To improve and optimize our service based on
|
||||
usage patterns
|
||||
</li>
|
||||
<li>
|
||||
To comply with legal obligations and enforce
|
||||
our terms
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The legal basis for processing your data includes: performance of our contract with you,
|
||||
your consent, our legitimate interests in improving the service, and compliance with legal obligations.
|
||||
The legal basis for processing your data
|
||||
includes: performance of our contract with you,
|
||||
your consent, our legitimate interests in
|
||||
improving the service, and compliance with legal
|
||||
obligations.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -96,18 +149,40 @@ export default function Privacy() {
|
|||
4. Data Security and Encryption
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We implement robust security measures to protect your personal information:
|
||||
We implement robust security measures to protect
|
||||
your personal information:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li><strong>End-to-End Encryption:</strong> Your financial data is encrypted on your device before being transmitted to our servers, ensuring that only you can access your information</li>
|
||||
<li><strong>Secure Storage:</strong> All data is stored on secure servers with encryption at rest</li>
|
||||
<li><strong>Access Controls:</strong> Strict access controls and authentication mechanisms protect against unauthorized access</li>
|
||||
<li><strong>Regular Security Audits:</strong> We regularly review and update our security practices</li>
|
||||
<li>
|
||||
<strong>End-to-End Encryption:</strong> Your
|
||||
financial data is encrypted on your device
|
||||
before being transmitted to our servers,
|
||||
ensuring that only you can access your
|
||||
information
|
||||
</li>
|
||||
<li>
|
||||
<strong>Secure Storage:</strong> All data is
|
||||
stored on secure servers with encryption at
|
||||
rest
|
||||
</li>
|
||||
<li>
|
||||
<strong>Access Controls:</strong> Strict
|
||||
access controls and authentication
|
||||
mechanisms protect against unauthorized
|
||||
access
|
||||
</li>
|
||||
<li>
|
||||
<strong>Regular Security Audits:</strong> We
|
||||
regularly review and update our security
|
||||
practices
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
While we use industry-standard security measures, no method of transmission over the internet
|
||||
or electronic storage is 100% secure. We cannot guarantee absolute security but continuously
|
||||
work to protect your data.
|
||||
While we use industry-standard security
|
||||
measures, no method of transmission over the
|
||||
internet or electronic storage is 100% secure.
|
||||
We cannot guarantee absolute security but
|
||||
continuously work to protect your data.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -116,14 +191,28 @@ export default function Privacy() {
|
|||
5. Third-Party Services
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We use the following third-party services to operate our platform:
|
||||
We use the following third-party services to
|
||||
operate our platform:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li><strong>Payment Processors:</strong> To process subscription payments and transactions. These processors have access only to the information necessary to perform their functions and are obligated to protect your data</li>
|
||||
<li><strong>Email Service Providers:</strong> To send transactional emails, password resets, and service notifications</li>
|
||||
<li>
|
||||
<strong>Payment Processors:</strong> To
|
||||
process subscription payments and
|
||||
transactions. These processors have access
|
||||
only to the information necessary to perform
|
||||
their functions and are obligated to protect
|
||||
your data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Email Service Providers:</strong> To
|
||||
send transactional emails, password resets,
|
||||
and service notifications
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We do not sell, trade, or rent your personal information to third parties for marketing purposes.
|
||||
We do not sell, trade, or rent your personal
|
||||
information to third parties for marketing
|
||||
purposes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -132,9 +221,12 @@ export default function Privacy() {
|
|||
6. Data Retention
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We retain your personal information for as long as necessary to provide our services and fulfill
|
||||
the purposes outlined in this Privacy Policy. When you delete your account, we will delete or
|
||||
anonymize your personal information within 30 days, except where we are required to retain it
|
||||
We retain your personal information for as long
|
||||
as necessary to provide our services and fulfill
|
||||
the purposes outlined in this Privacy Policy.
|
||||
When you delete your account, we will delete or
|
||||
anonymize your personal information within 30
|
||||
days, except where we are required to retain it
|
||||
for legal, accounting, or security purposes.
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -144,10 +236,13 @@ export default function Privacy() {
|
|||
7. International Data Transfers
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Your data is primarily stored and processed within the European Union. If we transfer data
|
||||
outside the EU, we ensure appropriate safeguards are in place, such as Standard Contractual
|
||||
Clauses approved by the European Commission, to protect your information in accordance with
|
||||
GDPR requirements.
|
||||
Your data is primarily stored and processed
|
||||
within the European Union. If we transfer data
|
||||
outside the EU, we ensure appropriate safeguards
|
||||
are in place, such as Standard Contractual
|
||||
Clauses approved by the European Commission, to
|
||||
protect your information in accordance with GDPR
|
||||
requirements.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -156,21 +251,55 @@ export default function Privacy() {
|
|||
8. Your Rights Under GDPR
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
As a user in the European Union, you have the following rights regarding your personal data:
|
||||
As a user in the European Union, you have the
|
||||
following rights regarding your personal data:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li><strong>Right of Access:</strong> You can request a copy of the personal data we hold about you</li>
|
||||
<li><strong>Right to Rectification:</strong> You can request correction of inaccurate or incomplete data</li>
|
||||
<li><strong>Right to Erasure:</strong> You can request deletion of your personal data (right to be forgotten)</li>
|
||||
<li><strong>Right to Data Portability:</strong> You can request your data in a structured, machine-readable format</li>
|
||||
<li><strong>Right to Restriction:</strong> You can request restriction of processing in certain circumstances</li>
|
||||
<li><strong>Right to Object:</strong> You can object to processing of your data based on legitimate interests</li>
|
||||
<li><strong>Right to Withdraw Consent:</strong> Where processing is based on consent, you can withdraw it at any time</li>
|
||||
<li><strong>Right to Lodge a Complaint:</strong> You can file a complaint with your local data protection authority</li>
|
||||
<li>
|
||||
<strong>Right of Access:</strong> You can
|
||||
request a copy of the personal data we hold
|
||||
about you
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Rectification:</strong> You
|
||||
can request correction of inaccurate or
|
||||
incomplete data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Erasure:</strong> You can
|
||||
request deletion of your personal data
|
||||
(right to be forgotten)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Data Portability:</strong>{' '}
|
||||
You can request your data in a structured,
|
||||
machine-readable format
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Restriction:</strong> You
|
||||
can request restriction of processing in
|
||||
certain circumstances
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Object:</strong> You can
|
||||
object to processing of your data based on
|
||||
legitimate interests
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Withdraw Consent:</strong>{' '}
|
||||
Where processing is based on consent, you
|
||||
can withdraw it at any time
|
||||
</li>
|
||||
<li>
|
||||
<strong>Right to Lodge a Complaint:</strong>{' '}
|
||||
You can file a complaint with your local
|
||||
data protection authority
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
To exercise any of these rights, please contact us at victor@whisper.money. We will respond
|
||||
to your request within 30 days.
|
||||
To exercise any of these rights, please contact
|
||||
us at victor@whisper.money. We will respond to
|
||||
your request within 30 days.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -179,9 +308,12 @@ export default function Privacy() {
|
|||
9. Cookies and Tracking
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We use essential cookies to maintain your session and ensure the proper functioning of our
|
||||
service. These cookies are necessary for the service to work and cannot be disabled. We do
|
||||
not use tracking cookies or analytics cookies without your explicit consent.
|
||||
We use essential cookies to maintain your
|
||||
session and ensure the proper functioning of our
|
||||
service. These cookies are necessary for the
|
||||
service to work and cannot be disabled. We do
|
||||
not use tracking cookies or analytics cookies
|
||||
without your explicit consent.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -190,9 +322,11 @@ export default function Privacy() {
|
|||
10. Children's Privacy
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Our service is not intended for users under the age of 16. We do not knowingly collect
|
||||
personal information from children. If you believe we have collected information from a
|
||||
child, please contact us immediately, and we will delete it.
|
||||
Our service is not intended for users under the
|
||||
age of 16. We do not knowingly collect personal
|
||||
information from children. If you believe we
|
||||
have collected information from a child, please
|
||||
contact us immediately, and we will delete it.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -201,11 +335,15 @@ export default function Privacy() {
|
|||
11. Changes to This Privacy Policy
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We may update this Privacy Policy from time to time to reflect changes in our practices or
|
||||
for legal, operational, or regulatory reasons. When we make material changes, we will notify
|
||||
you by email and/or by posting a notice on our website at least 30 days before the changes
|
||||
take effect. Your continued use of the service after changes become effective constitutes
|
||||
acceptance of the updated policy.
|
||||
We may update this Privacy Policy from time to
|
||||
time to reflect changes in our practices or for
|
||||
legal, operational, or regulatory reasons. When
|
||||
we make material changes, we will notify you by
|
||||
email and/or by posting a notice on our website
|
||||
at least 30 days before the changes take effect.
|
||||
Your continued use of the service after changes
|
||||
become effective constitutes acceptance of the
|
||||
updated policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -214,12 +352,18 @@ export default function Privacy() {
|
|||
12. Contact Us
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
If you have any questions, concerns, or requests regarding this Privacy Policy or our data
|
||||
If you have any questions, concerns, or requests
|
||||
regarding this Privacy Policy or our data
|
||||
practices, please contact us:
|
||||
</p>
|
||||
<div className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<p><strong>Email:</strong> victor@whisper.money</p>
|
||||
<p><strong>Address:</strong> Whisper Money, Calle Oca, Madrid - 28025, Spain</p>
|
||||
<p>
|
||||
<strong>Email:</strong> victor@whisper.money
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> Whisper Money,
|
||||
Calle Oca, Madrid - 28025, Spain
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -228,4 +372,3 @@ export default function Privacy() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,13 @@ function AccountActions({
|
|||
);
|
||||
}
|
||||
|
||||
function AccountRow({ row, onSuccess }: { row: Row<Account>; onSuccess?: () => void }) {
|
||||
function AccountRow({
|
||||
row,
|
||||
onSuccess,
|
||||
}: {
|
||||
row: Row<Account>;
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const account = row.original;
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
|
@ -130,13 +136,9 @@ function AccountRow({ row, onSuccess }: { row: Row<Account>; onSuccess?: () => v
|
|||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell: Cell<Account, unknown>) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
>
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
|
|
@ -348,13 +350,17 @@ export default function Accounts() {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<AccountRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
onSuccess={handleAccountCreated}
|
||||
/>
|
||||
))
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map((row) => (
|
||||
<AccountRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
onSuccess={
|
||||
handleAccountCreated
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import {
|
|||
VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import * as Icons from 'lucide-react';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { index as automationRulesIndex } from '@/actions/App/Http/Controllers/Settings/AutomationRuleController';
|
||||
import { CreateAutomationRuleDialog } from '@/components/automation-rules/create-automation-rule-dialog';
|
||||
|
|
@ -122,13 +122,9 @@ function AutomationRuleRow({ row }: { row: Row<AutomationRule> }) {
|
|||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell: Cell<AutomationRule, unknown>) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
>
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
|
|
@ -353,9 +349,14 @@ export default function AutomationRules() {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<AutomationRuleRow key={row.id} row={row} />
|
||||
))
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map((row) => (
|
||||
<AutomationRuleRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
|
|
|
|||
|
|
@ -122,13 +122,9 @@ function CategoryRow({ row }: { row: Row<Category> }) {
|
|||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell: Cell<Category, unknown>) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
>
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
|
|
@ -193,9 +189,9 @@ export default function Categories() {
|
|||
},
|
||||
cell: ({ row }) => {
|
||||
const iconName = row.original.icon;
|
||||
const IconComponent = Icons[
|
||||
iconName as keyof typeof Icons
|
||||
] as Icons.LucideIcon | undefined;
|
||||
const IconComponent = Icons[iconName as keyof typeof Icons] as
|
||||
| Icons.LucideIcon
|
||||
| undefined;
|
||||
const Icon = IconComponent || Icons.Tag;
|
||||
|
||||
return (
|
||||
|
|
@ -313,9 +309,14 @@ export default function Categories() {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<CategoryRow key={row.id} row={row} />
|
||||
))
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map((row) => (
|
||||
<CategoryRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
import { type SharedData } from '@/types';
|
||||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
|
||||
export default function Terms() {
|
||||
const { appUrl } = usePage<SharedData>().props;
|
||||
|
|
@ -14,7 +14,10 @@ export default function Terms() {
|
|||
<link rel="canonical" href={`${appUrl}/terms`} />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<meta property="og:title" content="Terms of Service - Whisper Money" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Terms of Service - Whisper Money"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Terms of service for Whisper Money. Review the rules and regulations for using our platform."
|
||||
|
|
@ -35,7 +38,7 @@ export default function Terms() {
|
|||
Terms of Service
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-neutral max-w-none dark:prose-invert">
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none">
|
||||
<p className="text-lg text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Last updated: {new Date().toLocaleDateString()}
|
||||
</p>
|
||||
|
|
@ -45,12 +48,20 @@ export default function Terms() {
|
|||
1. Service Provider
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
These Terms of Service govern your use of the Whisper Money personal finance platform.
|
||||
These Terms of Service govern your use of the
|
||||
Whisper Money personal finance platform.
|
||||
</p>
|
||||
<div className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<p><strong>Company Name:</strong> Whisper Money</p>
|
||||
<p><strong>Address:</strong> Calle Oca, Madrid - 28025, Spain</p>
|
||||
<p><strong>Email:</strong> victor@whisper.money</p>
|
||||
<p>
|
||||
<strong>Company Name:</strong> Whisper Money
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> Calle Oca, Madrid
|
||||
- 28025, Spain
|
||||
</p>
|
||||
<p>
|
||||
<strong>Email:</strong> victor@whisper.money
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -59,13 +70,17 @@ export default function Terms() {
|
|||
2. Acceptance of Terms
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
By creating an account or using Whisper Money, you agree to be bound by these Terms of
|
||||
Service and our Privacy Policy. If you do not agree to these terms, you may not use our
|
||||
service. These terms constitute a legally binding agreement between you and Whisper Money.
|
||||
By creating an account or using Whisper Money,
|
||||
you agree to be bound by these Terms of Service
|
||||
and our Privacy Policy. If you do not agree to
|
||||
these terms, you may not use our service. These
|
||||
terms constitute a legally binding agreement
|
||||
between you and Whisper Money.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You must be at least 16 years old to use this service. By using Whisper Money, you
|
||||
represent and warrant that you meet this age requirement.
|
||||
You must be at least 16 years old to use this
|
||||
service. By using Whisper Money, you represent
|
||||
and warrant that you meet this age requirement.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -74,18 +89,33 @@ export default function Terms() {
|
|||
3. Service Description
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Whisper Money is a personal finance tracking application that allows you to:
|
||||
Whisper Money is a personal finance tracking
|
||||
application that allows you to:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li>Manually record and track your financial transactions, budgets, and expenses</li>
|
||||
<li>Organize financial data with categories and custom labels</li>
|
||||
<li>Sync your encrypted financial data across multiple devices via cloud storage</li>
|
||||
<li>Access your data with end-to-end encryption to ensure privacy and security</li>
|
||||
<li>
|
||||
Manually record and track your financial
|
||||
transactions, budgets, and expenses
|
||||
</li>
|
||||
<li>
|
||||
Organize financial data with categories and
|
||||
custom labels
|
||||
</li>
|
||||
<li>
|
||||
Sync your encrypted financial data across
|
||||
multiple devices via cloud storage
|
||||
</li>
|
||||
<li>
|
||||
Access your data with end-to-end encryption
|
||||
to ensure privacy and security
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We reserve the right to modify, suspend, or discontinue any part of the service at any
|
||||
time, with or without notice. We will not be liable to you or any third party for any
|
||||
modification, suspension, or discontinuation of the service.
|
||||
We reserve the right to modify, suspend, or
|
||||
discontinue any part of the service at any time,
|
||||
with or without notice. We will not be liable to
|
||||
you or any third party for any modification,
|
||||
suspension, or discontinuation of the service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -94,20 +124,41 @@ export default function Terms() {
|
|||
4. User Accounts and Responsibilities
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
When you create an account with Whisper Money, you agree to:
|
||||
When you create an account with Whisper Money,
|
||||
you agree to:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li>Provide accurate, current, and complete information during registration</li>
|
||||
<li>Maintain and promptly update your account information</li>
|
||||
<li>Maintain the security and confidentiality of your account credentials</li>
|
||||
<li>Accept responsibility for all activities that occur under your account</li>
|
||||
<li>Notify us immediately of any unauthorized access or security breach</li>
|
||||
<li>Use the service only for lawful purposes and in compliance with all applicable laws</li>
|
||||
<li>
|
||||
Provide accurate, current, and complete
|
||||
information during registration
|
||||
</li>
|
||||
<li>
|
||||
Maintain and promptly update your account
|
||||
information
|
||||
</li>
|
||||
<li>
|
||||
Maintain the security and confidentiality of
|
||||
your account credentials
|
||||
</li>
|
||||
<li>
|
||||
Accept responsibility for all activities
|
||||
that occur under your account
|
||||
</li>
|
||||
<li>
|
||||
Notify us immediately of any unauthorized
|
||||
access or security breach
|
||||
</li>
|
||||
<li>
|
||||
Use the service only for lawful purposes and
|
||||
in compliance with all applicable laws
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You may not use the service to engage in any illegal activity, transmit malicious code,
|
||||
attempt to gain unauthorized access to our systems, or interfere with the proper functioning
|
||||
of the service.
|
||||
You may not use the service to engage in any
|
||||
illegal activity, transmit malicious code,
|
||||
attempt to gain unauthorized access to our
|
||||
systems, or interfere with the proper
|
||||
functioning of the service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -116,18 +167,24 @@ export default function Terms() {
|
|||
5. Data Ownership and License
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<strong>Your Data:</strong> You retain all ownership rights to the financial data and
|
||||
information you enter into Whisper Money. We do not claim any ownership over your personal
|
||||
<strong>Your Data:</strong> You retain all
|
||||
ownership rights to the financial data and
|
||||
information you enter into Whisper Money. We do
|
||||
not claim any ownership over your personal
|
||||
financial information.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
By using our service, you grant us a limited license to store, process, and transmit your
|
||||
data solely for the purpose of providing the service to you. This license terminates when
|
||||
you delete your data or close your account.
|
||||
By using our service, you grant us a limited
|
||||
license to store, process, and transmit your
|
||||
data solely for the purpose of providing the
|
||||
service to you. This license terminates when you
|
||||
delete your data or close your account.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You are responsible for maintaining backups of your data. While we implement reasonable
|
||||
backup procedures, we recommend you export and save copies of important data regularly.
|
||||
You are responsible for maintaining backups of
|
||||
your data. While we implement reasonable backup
|
||||
procedures, we recommend you export and save
|
||||
copies of important data regularly.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -136,14 +193,19 @@ export default function Terms() {
|
|||
6. Intellectual Property Rights
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The Whisper Money platform, including its software, design, text, graphics, logos, and
|
||||
other content (excluding your personal data), is owned by Whisper Money and protected by
|
||||
copyright, trademark, and other intellectual property laws. You may not copy, modify,
|
||||
distribute, sell, or lease any part of our service or software without our explicit written
|
||||
The Whisper Money platform, including its
|
||||
software, design, text, graphics, logos, and
|
||||
other content (excluding your personal data), is
|
||||
owned by Whisper Money and protected by
|
||||
copyright, trademark, and other intellectual
|
||||
property laws. You may not copy, modify,
|
||||
distribute, sell, or lease any part of our
|
||||
service or software without our explicit written
|
||||
permission.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The Whisper Money name and logo are trademarks of Whisper Money. You may not use these
|
||||
The Whisper Money name and logo are trademarks
|
||||
of Whisper Money. You may not use these
|
||||
trademarks without our prior written consent.
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -153,19 +215,42 @@ export default function Terms() {
|
|||
7. Payment Terms
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
If you subscribe to a paid plan or purchase premium features:
|
||||
If you subscribe to a paid plan or purchase
|
||||
premium features:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li>You agree to pay all applicable fees as described at the time of purchase</li>
|
||||
<li>Payments are processed by third-party payment processors and subject to their terms</li>
|
||||
<li>Subscription fees are billed in advance on a recurring basis until cancelled</li>
|
||||
<li>All fees are non-refundable except as required by law or explicitly stated otherwise</li>
|
||||
<li>We reserve the right to change our pricing with 30 days advance notice to subscribers</li>
|
||||
<li>You are responsible for all taxes associated with your purchase</li>
|
||||
<li>
|
||||
You agree to pay all applicable fees as
|
||||
described at the time of purchase
|
||||
</li>
|
||||
<li>
|
||||
Payments are processed by third-party
|
||||
payment processors and subject to their
|
||||
terms
|
||||
</li>
|
||||
<li>
|
||||
Subscription fees are billed in advance on a
|
||||
recurring basis until cancelled
|
||||
</li>
|
||||
<li>
|
||||
All fees are non-refundable except as
|
||||
required by law or explicitly stated
|
||||
otherwise
|
||||
</li>
|
||||
<li>
|
||||
We reserve the right to change our pricing
|
||||
with 30 days advance notice to subscribers
|
||||
</li>
|
||||
<li>
|
||||
You are responsible for all taxes associated
|
||||
with your purchase
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You may cancel your subscription at any time. Upon cancellation, you will retain access
|
||||
until the end of your current billing period, after which your subscription will not renew.
|
||||
You may cancel your subscription at any time.
|
||||
Upon cancellation, you will retain access until
|
||||
the end of your current billing period, after
|
||||
which your subscription will not renew.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -174,19 +259,25 @@ export default function Terms() {
|
|||
8. Termination
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You may terminate your account at any time by deleting your account through the application
|
||||
settings or by contacting us. Upon termination, your right to use the service will
|
||||
immediately cease.
|
||||
You may terminate your account at any time by
|
||||
deleting your account through the application
|
||||
settings or by contacting us. Upon termination,
|
||||
your right to use the service will immediately
|
||||
cease.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We may suspend or terminate your account if you violate these Terms, engage in fraudulent
|
||||
activity, or for any other reason at our discretion. We will provide notice when reasonably
|
||||
possible, but we reserve the right to immediately terminate accounts in cases of serious
|
||||
violations or legal requirements.
|
||||
We may suspend or terminate your account if you
|
||||
violate these Terms, engage in fraudulent
|
||||
activity, or for any other reason at our
|
||||
discretion. We will provide notice when
|
||||
reasonably possible, but we reserve the right to
|
||||
immediately terminate accounts in cases of
|
||||
serious violations or legal requirements.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Upon termination, we will delete your personal data in accordance with our Privacy Policy
|
||||
and applicable law, typically within 30 days.
|
||||
Upon termination, we will delete your personal
|
||||
data in accordance with our Privacy Policy and
|
||||
applicable law, typically within 30 days.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -195,21 +286,32 @@ export default function Terms() {
|
|||
9. Disclaimers and Warranties
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<strong>Financial Advice Disclaimer:</strong> Whisper Money is a tool for tracking and
|
||||
organizing your financial information. It does not provide financial, investment, tax, or
|
||||
legal advice. You should consult with qualified professionals for such advice. We are not
|
||||
responsible for any financial decisions you make based on data in the application.
|
||||
<strong>Financial Advice Disclaimer:</strong>{' '}
|
||||
Whisper Money is a tool for tracking and
|
||||
organizing your financial information. It does
|
||||
not provide financial, investment, tax, or legal
|
||||
advice. You should consult with qualified
|
||||
professionals for such advice. We are not
|
||||
responsible for any financial decisions you make
|
||||
based on data in the application.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<strong>Service Availability:</strong> The service is provided "as is" and "as available"
|
||||
without warranties of any kind. We do not guarantee that the service will be uninterrupted,
|
||||
error-free, or completely secure. We strive to maintain high availability but do not
|
||||
warrant that the service will meet your specific requirements.
|
||||
<strong>Service Availability:</strong> The
|
||||
service is provided "as is" and "as available"
|
||||
without warranties of any kind. We do not
|
||||
guarantee that the service will be
|
||||
uninterrupted, error-free, or completely secure.
|
||||
We strive to maintain high availability but do
|
||||
not warrant that the service will meet your
|
||||
specific requirements.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<strong>Data Accuracy:</strong> You are responsible for the accuracy of the data you enter.
|
||||
We do not verify or guarantee the accuracy of your financial information. Any calculations
|
||||
or reports generated by the service are based on the data you provide.
|
||||
<strong>Data Accuracy:</strong> You are
|
||||
responsible for the accuracy of the data you
|
||||
enter. We do not verify or guarantee the
|
||||
accuracy of your financial information. Any
|
||||
calculations or reports generated by the service
|
||||
are based on the data you provide.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -218,27 +320,51 @@ export default function Terms() {
|
|||
10. Limitation of Liability
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
To the maximum extent permitted by applicable law, Whisper Money, its directors, employees,
|
||||
and affiliates shall not be liable for any indirect, incidental, special, consequential, or
|
||||
punitive damages, including without limitation: loss of profits, data, use, goodwill, or
|
||||
other intangible losses resulting from:
|
||||
To the maximum extent permitted by applicable
|
||||
law, Whisper Money, its directors, employees,
|
||||
and affiliates shall not be liable for any
|
||||
indirect, incidental, special, consequential, or
|
||||
punitive damages, including without limitation:
|
||||
loss of profits, data, use, goodwill, or other
|
||||
intangible losses resulting from:
|
||||
</p>
|
||||
<ul className="mb-4 list-disc pl-6 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<li>Your use or inability to use the service</li>
|
||||
<li>Any unauthorized access to or use of our servers and/or personal information</li>
|
||||
<li>Any interruption or cessation of transmission to or from the service</li>
|
||||
<li>Any bugs, viruses, or similar harmful components transmitted through the service</li>
|
||||
<li>Any errors or omissions in content or any loss or damage incurred from using content</li>
|
||||
<li>Financial decisions made based on data in the application</li>
|
||||
<li>
|
||||
Your use or inability to use the service
|
||||
</li>
|
||||
<li>
|
||||
Any unauthorized access to or use of our
|
||||
servers and/or personal information
|
||||
</li>
|
||||
<li>
|
||||
Any interruption or cessation of
|
||||
transmission to or from the service
|
||||
</li>
|
||||
<li>
|
||||
Any bugs, viruses, or similar harmful
|
||||
components transmitted through the service
|
||||
</li>
|
||||
<li>
|
||||
Any errors or omissions in content or any
|
||||
loss or damage incurred from using content
|
||||
</li>
|
||||
<li>
|
||||
Financial decisions made based on data in
|
||||
the application
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Our total liability to you for all claims arising from or related to the service shall not
|
||||
exceed the amount you paid us in the 12 months prior to the claim, or €100, whichever is
|
||||
Our total liability to you for all claims
|
||||
arising from or related to the service shall not
|
||||
exceed the amount you paid us in the 12 months
|
||||
prior to the claim, or €100, whichever is
|
||||
greater.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Nothing in these Terms excludes or limits our liability for death or personal injury caused
|
||||
by negligence, fraud, or any liability that cannot be excluded or limited under applicable law.
|
||||
Nothing in these Terms excludes or limits our
|
||||
liability for death or personal injury caused by
|
||||
negligence, fraud, or any liability that cannot
|
||||
be excluded or limited under applicable law.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -247,10 +373,14 @@ export default function Terms() {
|
|||
11. Indemnification
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
You agree to indemnify, defend, and hold harmless Whisper Money and its officers, directors,
|
||||
employees, and agents from any claims, liabilities, damages, losses, and expenses, including
|
||||
reasonable legal fees, arising out of or related to your use of the service, violation of
|
||||
these Terms, or violation of any rights of another party.
|
||||
You agree to indemnify, defend, and hold
|
||||
harmless Whisper Money and its officers,
|
||||
directors, employees, and agents from any
|
||||
claims, liabilities, damages, losses, and
|
||||
expenses, including reasonable legal fees,
|
||||
arising out of or related to your use of the
|
||||
service, violation of these Terms, or violation
|
||||
of any rights of another party.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -259,17 +389,23 @@ export default function Terms() {
|
|||
12. Governing Law and Dispute Resolution
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
These Terms shall be governed by and construed in accordance with the laws of Spain,
|
||||
without regard to its conflict of law provisions. You agree to submit to the exclusive
|
||||
jurisdiction of the courts located in Madrid, Spain for the resolution of any disputes.
|
||||
These Terms shall be governed by and construed
|
||||
in accordance with the laws of Spain, without
|
||||
regard to its conflict of law provisions. You
|
||||
agree to submit to the exclusive jurisdiction of
|
||||
the courts located in Madrid, Spain for the
|
||||
resolution of any disputes.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
For users in the European Union, nothing in these Terms affects your rights as a consumer
|
||||
under applicable consumer protection laws, including your right to bring proceedings in
|
||||
For users in the European Union, nothing in
|
||||
these Terms affects your rights as a consumer
|
||||
under applicable consumer protection laws,
|
||||
including your right to bring proceedings in
|
||||
your country of residence.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We encourage you to contact us directly at victor@whisper.money to resolve any disputes
|
||||
We encourage you to contact us directly at
|
||||
victor@whisper.money to resolve any disputes
|
||||
before initiating legal proceedings.
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -279,14 +415,19 @@ export default function Terms() {
|
|||
13. Changes to Terms
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We reserve the right to modify or replace these Terms at any time. If a revision is
|
||||
material, we will provide at least 30 days notice prior to the new terms taking effect by
|
||||
sending you an email notification and/or posting a notice on our website.
|
||||
We reserve the right to modify or replace these
|
||||
Terms at any time. If a revision is material, we
|
||||
will provide at least 30 days notice prior to
|
||||
the new terms taking effect by sending you an
|
||||
email notification and/or posting a notice on
|
||||
our website.
|
||||
</p>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Your continued use of the service after the effective date of revised Terms constitutes
|
||||
your acceptance of the changes. If you do not agree to the new terms, you must stop using
|
||||
the service and may delete your account.
|
||||
Your continued use of the service after the
|
||||
effective date of revised Terms constitutes your
|
||||
acceptance of the changes. If you do not agree
|
||||
to the new terms, you must stop using the
|
||||
service and may delete your account.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -295,9 +436,12 @@ export default function Terms() {
|
|||
14. Severability
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
If any provision of these Terms is found to be invalid, illegal, or unenforceable, the
|
||||
remaining provisions shall continue in full force and effect. The invalid provision shall
|
||||
be modified to the minimum extent necessary to make it valid and enforceable.
|
||||
If any provision of these Terms is found to be
|
||||
invalid, illegal, or unenforceable, the
|
||||
remaining provisions shall continue in full
|
||||
force and effect. The invalid provision shall be
|
||||
modified to the minimum extent necessary to make
|
||||
it valid and enforceable.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -306,8 +450,10 @@ export default function Terms() {
|
|||
15. Entire Agreement
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
These Terms, together with our Privacy Policy, constitute the entire agreement between you
|
||||
and Whisper Money regarding the use of our service and supersede any prior agreements or
|
||||
These Terms, together with our Privacy Policy,
|
||||
constitute the entire agreement between you and
|
||||
Whisper Money regarding the use of our service
|
||||
and supersede any prior agreements or
|
||||
understandings, whether written or oral.
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -317,12 +463,17 @@ export default function Terms() {
|
|||
16. Contact Us
|
||||
</h2>
|
||||
<p className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
If you have any questions, concerns, or feedback about these Terms of Service, please
|
||||
contact us:
|
||||
If you have any questions, concerns, or feedback
|
||||
about these Terms of Service, please contact us:
|
||||
</p>
|
||||
<div className="mb-4 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<p><strong>Email:</strong> victor@whisper.money</p>
|
||||
<p><strong>Address:</strong> Whisper Money, Calle Oca, Madrid - 28025, Spain</p>
|
||||
<p>
|
||||
<strong>Email:</strong> victor@whisper.money
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> Whisper Money,
|
||||
Calle Oca, Madrid - 28025, Spain
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -331,4 +482,3 @@ export default function Terms() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,20 +103,24 @@ function TransactionRowComponent({
|
|||
return (
|
||||
<ContextMenu key={row.id} onOpenChange={setContextMenuOpen}>
|
||||
<ContextMenuTrigger asChild>
|
||||
{ }
|
||||
{}
|
||||
<TableRow
|
||||
ref={rowVirtualizer.measureElement}
|
||||
data-state={(row.getIsSelected() || contextMenuOpen) && 'selected'}
|
||||
data-state={
|
||||
(row.getIsSelected() || contextMenuOpen) && 'selected'
|
||||
}
|
||||
data-index={virtualRow.index}
|
||||
>
|
||||
{row.getVisibleCells().map((cell: Cell<DecryptedTransaction, unknown>) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell: Cell<DecryptedTransaction, unknown>) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
|
|
@ -1012,7 +1016,11 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
}
|
||||
|
||||
const renderTransactionRow = useCallback(
|
||||
(row: Row<DecryptedTransaction>, virtualRow: VirtualItem, rowVirtualizer: Virtualizer<HTMLDivElement, Element>) => {
|
||||
(
|
||||
row: Row<DecryptedTransaction>,
|
||||
virtualRow: VirtualItem,
|
||||
rowVirtualizer: Virtualizer<HTMLDivElement, Element>,
|
||||
) => {
|
||||
return (
|
||||
<TransactionRowComponent
|
||||
key={row.id}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,27 @@
|
|||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { LEAD_FUNNEL_EVENT_UUID } from '@/lib/constants';
|
||||
import { trackEvent } from '@/lib/track-event';
|
||||
import { dashboard } from '@/routes';
|
||||
import { store } from '@/routes/user-leads';
|
||||
import { type SharedData } from '@/types';
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Separator } from '@radix-ui/react-separator';
|
||||
import {
|
||||
LockIcon,
|
||||
ShieldCheckIcon,
|
||||
TrendingUpIcon,
|
||||
BellIcon,
|
||||
PieChartIcon,
|
||||
SmartphoneIcon,
|
||||
ZapIcon,
|
||||
EyeOffIcon,
|
||||
BirdIcon,
|
||||
CodeIcon,
|
||||
EyeOffIcon,
|
||||
Github,
|
||||
GithubIcon,
|
||||
LucideGithub,
|
||||
LockIcon,
|
||||
PieChartIcon,
|
||||
ShieldCheckIcon,
|
||||
SmartphoneIcon,
|
||||
TrendingUpIcon,
|
||||
ZapIcon,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { trackEvent } from '@/lib/track-event';
|
||||
import { LEAD_FUNNEL_EVENT_UUID } from '@/lib/constants';
|
||||
import { Separator } from '@radix-ui/react-separator';
|
||||
|
||||
export default function Welcome({
|
||||
canRegister = true,
|
||||
|
|
@ -60,38 +58,64 @@ export default function Welcome({
|
|||
<link rel="canonical" href={appUrl} />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Stack+Sans+Text:wght@200..700&display=swap" rel="stylesheet" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Stack+Sans+Text:wght@200..700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<meta property="og:site_name" content="Whisper Money" />
|
||||
<meta property="og:title" content="Whisper Money - The Most Secure Personal Finance App" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Whisper Money - The Most Secure Personal Finance App"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Your financial data stays private with end-to-end encryption. The most secure way to manage your personal finances."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={appUrl} />
|
||||
<meta property="og:image" content={`${appUrl}/images/og_whisper_money.png`} />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`${appUrl}/images/og_whisper_money.png`}
|
||||
/>
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Whisper Money - Secure Personal Finance App" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="Whisper Money - Secure Personal Finance App"
|
||||
/>
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Whisper Money - The Most Secure Personal Finance App" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Whisper Money - The Most Secure Personal Finance App"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Your financial data stays private with end-to-end encryption. The most secure way to manage your personal finances."
|
||||
/>
|
||||
<meta name="twitter:image" content={`${appUrl}/images/og_whisper_money.png`} />
|
||||
<meta name="twitter:image:alt" content="Whisper Money - Secure Personal Finance App" />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content={`${appUrl}/images/og_whisper_money.png`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image:alt"
|
||||
content="Whisper Money - Secure Personal Finance App"
|
||||
/>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebApplication',
|
||||
name: 'Whisper Money',
|
||||
description: 'The most secure personal finance app with end-to-end encryption. Track expenses, create budgets, and manage your money privately.',
|
||||
description:
|
||||
'The most secure personal finance app with end-to-end encryption. Track expenses, create budgets, and manage your money privately.',
|
||||
url: appUrl,
|
||||
applicationCategory: 'FinanceApplication',
|
||||
offers: {
|
||||
|
|
@ -111,11 +135,11 @@ export default function Welcome({
|
|||
</script>
|
||||
</Head>
|
||||
<div className="flex min-h-screen flex-col bg-[#FDFDFC] text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#EDEDEC]">
|
||||
<header className="w-full fade-bottom bg-background/5 fixed top-0 z-50 backdrop-blur-lg">
|
||||
<header className="fade-bottom fixed top-0 z-50 w-full bg-background/5 backdrop-blur-lg">
|
||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-2 py-4 lg:py-6">
|
||||
<div className="flex items-center gap-4 font-mono">
|
||||
<BirdIcon className="size-5 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
<span className='font-medium'>Whisper Money</span>
|
||||
<span className="font-medium">Whisper Money</span>
|
||||
</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
<a
|
||||
|
|
@ -123,38 +147,42 @@ export default function Welcome({
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant={'ghost'} className='cursor-pointer opacity-70 hover:opacity-100 transition-all duration-200'>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="cursor-pointer opacity-70 transition-all duration-200 hover:opacity-100"
|
||||
>
|
||||
<Github className="size-5" />
|
||||
Github
|
||||
</Button>
|
||||
</a>
|
||||
{!hideAuthButtons && (
|
||||
<>
|
||||
<Separator orientation="vertical"
|
||||
className="data-[orientation=vertical]:bg-border data-[orientation=vertical]:w-[1px] data-[orientation=vertical]:h-6"
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="data-[orientation=vertical]:h-6 data-[orientation=vertical]:w-[1px] data-[orientation=vertical]:bg-border"
|
||||
/>
|
||||
{auth.user ? (
|
||||
<Link
|
||||
href={dashboard()}
|
||||
>
|
||||
<Button className='cursor-pointer'>
|
||||
<Link href={dashboard()}>
|
||||
<Button className="cursor-pointer">
|
||||
Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
>
|
||||
<Button variant={'ghost'} className='cursor-pointer'>
|
||||
<Link href="/login">
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Log in
|
||||
</Button>
|
||||
</Link>
|
||||
{canRegister && (
|
||||
<Link
|
||||
href="/register"
|
||||
>
|
||||
<Button variant='default' className='cursor-pointer'>
|
||||
<Link href="/register">
|
||||
<Button
|
||||
variant="default"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Link>
|
||||
|
|
@ -170,17 +198,18 @@ export default function Welcome({
|
|||
<main className="flex flex-1 flex-col">
|
||||
<section className="relative w-full overflow-hidden px-6 py-24 sm:py-32 md:py-40">
|
||||
<div className="relative z-10 mx-auto flex max-w-7xl flex-col gap-12">
|
||||
<div className="max-w-5xl w-full mx-auto relative z-10 flex flex-col items-start gap-2 sm:gap-8">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-[#e3e3e0] py-1 px-2.5 text-[0.8rem] font-medium dark:border-[#3E3E3A]">
|
||||
<LockIcon className='size-3.5 opacity-75' />
|
||||
<div className="relative z-10 mx-auto flex w-full max-w-5xl flex-col items-start gap-2 sm:gap-8">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-[#e3e3e0] px-2.5 py-1 text-[0.8rem] font-medium dark:border-[#3E3E3A]">
|
||||
<LockIcon className="size-3.5 opacity-75" />
|
||||
<span className="text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Military Grade Encryption
|
||||
</span>
|
||||
</span>
|
||||
<h1 className="max-w-[840px] bg-gradient-to-r from-[#1b1b18] to-[#1b1b18] bg-clip-text text-4xl font-semibold leading-tight text-transparent drop-shadow-2xl dark:from-[#EDEDEC] dark:to-[#A1A09A] sm:text-5xl sm:leading-tight lg:text-6xl lg:leading-tight font-heading">
|
||||
The most secure way to understand your finances
|
||||
<h1 className="font-heading max-w-[840px] bg-gradient-to-r from-[#1b1b18] to-[#1b1b18] bg-clip-text text-4xl leading-tight font-semibold text-transparent drop-shadow-2xl sm:text-5xl sm:leading-tight lg:text-6xl lg:leading-tight dark:from-[#EDEDEC] dark:to-[#A1A09A]">
|
||||
The most secure way to understand your
|
||||
finances
|
||||
</h1>
|
||||
<p className="max-w-[840px] mb-4 text-lg leading-8 font-medium text-[#706f6c] dark:text-[#A1A09A] lg:text-xl lg:leading-8">
|
||||
<p className="mb-4 max-w-[840px] text-lg leading-8 font-medium text-[#706f6c] lg:text-xl lg:leading-8 dark:text-[#A1A09A]">
|
||||
Your financial data stays private with
|
||||
end-to-end encryption. Track expenses,
|
||||
create budgets, and achieve your goals—all
|
||||
|
|
@ -190,7 +219,7 @@ export default function Welcome({
|
|||
<div className="flex w-full max-w-lg flex-col gap-4">
|
||||
<Form
|
||||
{...store.form()}
|
||||
className="flex sm:items-center justify-center flex-col gap-2 sm:flex-row"
|
||||
className="flex flex-col justify-center gap-2 sm:flex-row sm:items-center"
|
||||
>
|
||||
{({ processing, errors }) => (
|
||||
<>
|
||||
|
|
@ -206,7 +235,7 @@ export default function Welcome({
|
|||
<Button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
className="h-12 bg-gradient-to-t px-6 from-zinc-700 to-zinc-900 dark:from-zinc-200 dark:to-zinc-300 text-shadow cursor-pointer disabled:cursor-default text-white shadow-sm transition-all duration hover:from-zinc-800 hover:to-black hover:shadow-md dark:bg-[#eeeeec] dark:text-[#1C1C1A] dark:hover:bg-white dark:hover:shadow-md hover:dark:from-zinc-50"
|
||||
className="text-shadow duration h-12 cursor-pointer bg-gradient-to-t from-zinc-700 to-zinc-900 px-6 text-white shadow-sm transition-all hover:from-zinc-800 hover:to-black hover:shadow-md disabled:cursor-default dark:bg-[#eeeeec] dark:from-zinc-200 dark:to-zinc-300 dark:text-[#1C1C1A] dark:hover:bg-white hover:dark:from-zinc-50 dark:hover:shadow-md"
|
||||
>
|
||||
{processing
|
||||
? 'Submitting...'
|
||||
|
|
@ -236,8 +265,16 @@ export default function Welcome({
|
|||
<div className="relative z-10 overflow-hidden rounded-2xl border border-[#e3e3e0]/50 bg-[#FDFDFC]/50 p-2 shadow-2xl dark:border-[#3E3E3A]/10 dark:bg-[#161615]/50">
|
||||
<div className="relative z-10 overflow-hidden rounded-md border border-[#e3e3e0]/70 shadow-2xl dark:border-[#3E3E3A]/5">
|
||||
<div className="aspect-[16/10] rounded-lg border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#0a0a0a]">
|
||||
<img src="/images/landing/bank-accounts.png" alt="Bank accounts securely stored" className="dark:hidden h-full w-auto rounded-lg" />
|
||||
<img src="/images/landing/dark_bank-accounts.png" alt="Bank accounts securely stored" className="hidden dark:block h-full w-auto rounded-lg" />
|
||||
<img
|
||||
src="/images/landing/bank-accounts.png"
|
||||
alt="Bank accounts securely stored"
|
||||
className="h-full w-auto rounded-lg dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/dark_bank-accounts.png"
|
||||
alt="Bank accounts securely stored"
|
||||
className="hidden h-full w-auto rounded-lg dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -246,8 +283,16 @@ export default function Welcome({
|
|||
<div className="relative z-10 overflow-hidden rounded-2xl border border-[#e3e3e0]/50 bg-[#FDFDFC]/50 p-2 shadow-2xl dark:border-[#3E3E3A]/10 dark:bg-[#161615]/50">
|
||||
<div className="relative z-10 overflow-hidden rounded-md border border-[#e3e3e0]/70 shadow-2xl dark:border-[#3E3E3A]/5">
|
||||
<div className="aspect-[16/10] rounded-lg border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#0a0a0a]">
|
||||
<img src="/images/landing/unlock-key.png" alt="Everything is encrypted with a private & local key you only have" className="dark:hidden h-full w-auto rounded-lg" />
|
||||
<img src="/images/landing/dark_unlock-key.png" alt="Everything is encrypted with a private & local key you only have" className="hidden dark:block h-full w-auto rounded-lg" />
|
||||
<img
|
||||
src="/images/landing/unlock-key.png"
|
||||
alt="Everything is encrypted with a private & local key you only have"
|
||||
className="h-full w-auto rounded-lg dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/dark_unlock-key.png"
|
||||
alt="Everything is encrypted with a private & local key you only have"
|
||||
className="hidden h-full w-auto rounded-lg dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -256,8 +301,16 @@ export default function Welcome({
|
|||
<div className="relative z-10 overflow-hidden rounded-2xl border border-[#e3e3e0]/50 bg-[#FDFDFC]/50 p-2 shadow-2xl dark:border-[#3E3E3A]/10 dark:bg-[#161615]/50">
|
||||
<div className="relative z-10 overflow-hidden rounded-md border border-[#e3e3e0]/70 shadow-2xl dark:border-[#3E3E3A]/5">
|
||||
<div className="aspect-[16/10] rounded-lg border-[#e3e3e0] bg-[#FDFDFC] dark:border-[#3E3E3A] dark:bg-[#0a0a0a]">
|
||||
<img src="/images/landing/transactions.png" alt="You're transactions with a military grade encryption" className="dark:hidden h-full w-auto rounded-lg" />
|
||||
<img src="/images/landing/dark_transactions.png" alt="You're transactions with a military grade encryption" className="hidden dark:block h-full w-auto rounded-lg" />
|
||||
<img
|
||||
src="/images/landing/transactions.png"
|
||||
alt="You're transactions with a military grade encryption"
|
||||
className="h-full w-auto rounded-lg dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/images/landing/dark_transactions.png"
|
||||
alt="You're transactions with a military grade encryption"
|
||||
className="hidden h-full w-auto rounded-lg dark:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -265,7 +318,7 @@ export default function Welcome({
|
|||
|
||||
<div
|
||||
data-slot="glow"
|
||||
className="absolute top-[50%] mt-32 w-full animate-appear-zoom opacity-0 delay-2000 lg:mt-4"
|
||||
className="animate-appear-zoom absolute top-[50%] mt-32 w-full opacity-0 delay-2000 lg:mt-4"
|
||||
>
|
||||
<div className="absolute left-1/2 h-[256px] w-[60%] -translate-x-1/2 -translate-y-1/2 scale-[2.5] rounded-[50%] bg-radial from-[#1b1b18]/50 from-10% to-[#1b1b18]/0 to-60% opacity-20 sm:h-[512px] dark:from-[#EDEDEC]/50 dark:to-[#EDEDEC]/0 dark:opacity-100"></div>
|
||||
<div className="absolute left-1/2 h-[128px] w-[40%] -translate-x-1/2 -translate-y-1/2 scale-200 rounded-[50%] bg-radial from-[#1b1b18]/30 from-10% to-[#1b1b18]/0 to-60% opacity-20 sm:h-[256px] dark:from-[#EDEDEC]/30 dark:to-[#EDEDEC]/0 dark:opacity-100"></div>
|
||||
|
|
@ -274,131 +327,131 @@ export default function Welcome({
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-4 py-12 dark:border-[#3E3E3A] sm:py-24 md:py-32">
|
||||
<section className="px-4 py-12 sm:py-24 md:py-32 dark:border-[#3E3E3A]">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center gap-6 sm:gap-20">
|
||||
<h2 className="max-w-[560px] text-center text-3xl font-semibold leading-tight sm:text-5xl sm:leading-tight">
|
||||
<h2 className="max-w-[560px] text-center text-3xl leading-tight font-semibold sm:text-5xl sm:leading-tight">
|
||||
Everything you need. Nothing you don't.
|
||||
</h2>
|
||||
<div className="grid auto-rows-fr grid-cols-2 gap-0 sm:grid-cols-3 sm:gap-4 lg:grid-cols-4">
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<ShieldCheckIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
End-to-end encryption
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Your financial data is encrypted on your
|
||||
device. Only you can access it.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<TrendingUpIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Smart budgeting
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Create budgets that adapt to your
|
||||
spending habits and goals.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<BellIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Intelligent insights
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Are you overspending?
|
||||
Know exactly where you stand.
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Are you overspending? Know exactly where
|
||||
you stand.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<PieChartIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Visual insights
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Understand your spending with beautiful
|
||||
charts and reports.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<SmartphoneIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Works everywhere
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Access your finances on any device,
|
||||
anytime, anywhere.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<ZapIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Lightning fast
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Built for speed with instant sync and
|
||||
smooth interactions.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<EyeOffIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Zero tracking
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
We don't track, sell, or share your
|
||||
data. Ever.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold leading-none tracking-tight sm:text-base">
|
||||
<h3 className="flex items-center gap-2 text-sm leading-none font-semibold tracking-tight sm:text-base">
|
||||
<div className="flex items-center self-start">
|
||||
<CodeIcon className="size-5 stroke-1 text-[#1b1b18] dark:text-[#EDEDEC]" />
|
||||
</div>
|
||||
Open source
|
||||
</h3>
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-balance text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Fully transparent and open source. Review
|
||||
the code yourself.
|
||||
<div className="flex max-w-[240px] flex-col gap-2 text-sm text-balance text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Fully transparent and open source.
|
||||
Review the code yourself.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="w-full overflow-hidden px-0 py-12 dark:border-[#3E3E3A] sm:py-24 md:py-32">
|
||||
<section className="w-full overflow-hidden px-0 py-12 sm:py-24 md:py-32 dark:border-[#3E3E3A]">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center gap-4 px-1 text-center sm:gap-16">
|
||||
<div className="flex flex-col items-center gap-4 px-4 sm:gap-4">
|
||||
<h2 className="max-w-[720px] text-2xl font-semibold leading-tight sm:text-3xl sm:leading-tight">
|
||||
<h2 className="max-w-[720px] text-2xl leading-tight font-semibold sm:text-3xl sm:leading-tight">
|
||||
Trusted by people who value their privacy
|
||||
</h2>
|
||||
<p className="text-md max-w-[600px] font-medium text-[#706f6c] dark:text-[#A1A09A] sm:text-xl">
|
||||
<p className="text-md max-w-[600px] font-medium text-[#706f6c] sm:text-xl dark:text-[#A1A09A]">
|
||||
Join thousands of users who have taken
|
||||
control of their finances without
|
||||
compromising their privacy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative flex w-full flex-col items-center justify-center overflow-hidden">
|
||||
<div className="group flex flex-row overflow-hidden p-2 [--duration:20s] [--gap:1rem] [gap:var(--gap)]">
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [gap:var(--gap)]">
|
||||
<div className="group flex flex-row [gap:var(--gap)] overflow-hidden p-2 [--duration:20s] [--gap:1rem]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Sarah M.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -416,7 +469,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Michael R.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -434,7 +487,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Emma L.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -444,16 +497,16 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Love that my financial data is
|
||||
encrypted. No more worrying about
|
||||
data breaches!
|
||||
encrypted. No more worrying
|
||||
about data breaches!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Sarah M.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -471,7 +524,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Michael R.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -489,7 +542,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Emma L.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -499,16 +552,16 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Love that my financial data is
|
||||
encrypted. No more worrying about
|
||||
data breaches!
|
||||
encrypted. No more worrying
|
||||
about data breaches!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Sarah M.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -526,7 +579,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Michael R.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -544,7 +597,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Emma L.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -554,16 +607,16 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Love that my financial data is
|
||||
encrypted. No more worrying about
|
||||
data breaches!
|
||||
encrypted. No more worrying
|
||||
about data breaches!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Sarah M.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -581,7 +634,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Michael R.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -599,7 +652,7 @@ export default function Welcome({
|
|||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Emma L.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -609,18 +662,18 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Love that my financial data is
|
||||
encrypted. No more worrying about
|
||||
data breaches!
|
||||
encrypted. No more worrying
|
||||
about data breaches!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="group flex flex-row overflow-hidden p-2 [--duration:20s] [--gap:1rem] [gap:var(--gap)]">
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [animation-direction:reverse] [gap:var(--gap)]">
|
||||
<div className="group flex flex-row [gap:var(--gap)] overflow-hidden p-2 [--duration:20s] [--gap:1rem]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] [animation-direction:reverse] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
David K.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -631,13 +684,14 @@ export default function Welcome({
|
|||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
As a developer, I appreciate the
|
||||
security architecture. This is
|
||||
how finance apps should be built.
|
||||
how finance apps should be
|
||||
built.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Jessica P.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -647,14 +701,14 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The automation rules save me so
|
||||
much time. And knowing my data is
|
||||
private? Priceless.
|
||||
much time. And knowing my data
|
||||
is private? Priceless.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Alex T.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -670,11 +724,11 @@ export default function Welcome({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [animation-direction:reverse] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] [animation-direction:reverse] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
David K.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -685,13 +739,14 @@ export default function Welcome({
|
|||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
As a developer, I appreciate the
|
||||
security architecture. This is
|
||||
how finance apps should be built.
|
||||
how finance apps should be
|
||||
built.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Jessica P.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -701,14 +756,14 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The automation rules save me so
|
||||
much time. And knowing my data is
|
||||
private? Priceless.
|
||||
much time. And knowing my data
|
||||
is private? Priceless.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Alex T.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -724,11 +779,11 @@ export default function Welcome({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [animation-direction:reverse] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] [animation-direction:reverse] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
David K.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -739,13 +794,14 @@ export default function Welcome({
|
|||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
As a developer, I appreciate the
|
||||
security architecture. This is
|
||||
how finance apps should be built.
|
||||
how finance apps should be
|
||||
built.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Jessica P.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -755,14 +811,14 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The automation rules save me so
|
||||
much time. And knowing my data is
|
||||
private? Priceless.
|
||||
much time. And knowing my data
|
||||
is private? Priceless.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Alex T.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -778,11 +834,11 @@ export default function Welcome({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 animate-marquee flex-row justify-around group-hover:[animation-play-state:paused] [animation-direction:reverse] [gap:var(--gap)]">
|
||||
<div className="animate-marquee flex shrink-0 flex-row justify-around [gap:var(--gap)] [animation-direction:reverse] group-hover:[animation-play-state:paused]">
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
David K.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -793,13 +849,14 @@ export default function Welcome({
|
|||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
As a developer, I appreciate the
|
||||
security architecture. This is
|
||||
how finance apps should be built.
|
||||
how finance apps should be
|
||||
built.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Jessica P.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -809,14 +866,14 @@ export default function Welcome({
|
|||
</div>
|
||||
<p className="sm:text-md mt-4 text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
The automation rules save me so
|
||||
much time. And knowing my data is
|
||||
private? Priceless.
|
||||
much time. And knowing my data
|
||||
is private? Priceless.
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass-3 flex max-w-[320px] flex-col rounded-lg p-4 text-start shadow-md sm:max-w-[420px] sm:p-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col items-start">
|
||||
<h3 className="text-md font-semibold leading-none">
|
||||
<h3 className="text-md leading-none font-semibold">
|
||||
Alex T.
|
||||
</h3>
|
||||
<p className="text-sm text-[#706f6c] dark:text-[#A1A09A]">
|
||||
|
|
@ -833,15 +890,15 @@ export default function Welcome({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="from-background pointer-events-none absolute inset-y-0 left-0 hidden w-1/3 bg-linear-to-r sm:block"></div>
|
||||
<div className="from-background pointer-events-none absolute inset-y-0 right-0 hidden w-1/3 bg-linear-to-l sm:block"></div>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 hidden w-1/3 bg-linear-to-r from-background sm:block"></div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 hidden w-1/3 bg-linear-to-l from-background sm:block"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="py-8 dark:border-[#3E3E3A]">
|
||||
<div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-4 px-6 text-sm text-[#706f6c] dark:text-[#A1A09A] sm:flex-row lg:px-8">
|
||||
<div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-4 px-6 text-sm text-[#706f6c] sm:flex-row lg:px-8 dark:text-[#A1A09A]">
|
||||
<p>
|
||||
© {new Date().getFullYear()} Whisper Money. All
|
||||
rights reserved.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { db } from '@/lib/dexie-db';
|
||||
import { SyncManager } from '@/lib/sync-manager';
|
||||
import type { AccountBalance } from '@/types/account';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
class AccountBalanceSyncService {
|
||||
private syncManager: SyncManager;
|
||||
|
|
@ -71,7 +70,10 @@ class AccountBalanceSyncService {
|
|||
throw new Error('Balance not found');
|
||||
}
|
||||
|
||||
return await this.syncManager.update<AccountBalance>(id.toString(), data);
|
||||
return await this.syncManager.update<AccountBalance>(
|
||||
id.toString(),
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
async updateOrCreate(
|
||||
|
|
@ -120,4 +122,3 @@ class AccountBalanceSyncService {
|
|||
}
|
||||
|
||||
export const accountBalanceSyncService = new AccountBalanceSyncService();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ class AccountSyncService {
|
|||
}
|
||||
|
||||
async create(data: Omit<Account, 'id'>): Promise<Account> {
|
||||
return await this.syncManager.createLocal<Account>(data as Omit<Account, 'id' | 'created_at' | 'updated_at'>);
|
||||
return await this.syncManager.createLocal<Account>(
|
||||
data as Omit<Account, 'id' | 'created_at' | 'updated_at'>,
|
||||
);
|
||||
}
|
||||
|
||||
async update(id: UUID, data: Partial<Account>): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ class CategorySyncService {
|
|||
}
|
||||
|
||||
async create(data: Omit<Category, 'id'>): Promise<Category> {
|
||||
return await this.syncManager.createLocal<Category>(data as Omit<Category, 'id' | 'created_at' | 'updated_at'>);
|
||||
return await this.syncManager.createLocal<Category>(
|
||||
data as Omit<Category, 'id' | 'created_at' | 'updated_at'>,
|
||||
);
|
||||
}
|
||||
|
||||
async update(id: UUID, data: Partial<Category>): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export type UUID = string;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue