diff --git a/frontend/package.json b/frontend/package.json index dabbd76..f43ed1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "api:generate": "node scripts/generate-api.mjs" }, "dependencies": { + "@fontsource/funnel-sans": "^5.3.0", + "@fontsource/geist-mono": "^5.3.0", "@hookform/resolvers": "^5.1.0", "@tanstack/react-query": "^5.80.0", "i18next": "^25.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a0b7ed2..7d614fa 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@fontsource/funnel-sans': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource/geist-mono': + specifier: ^5.3.0 + version: 5.3.0 '@hookform/resolvers': specifier: ^5.1.0 version: 5.9.1(react-hook-form@7.87.0(react@19.3.0))(zod@3.25.76) @@ -59,22 +65,22 @@ importers: version: 19.3.0(@types/react@19.3.0) '@vitejs/plugin-react': specifier: ^4.5.0 - version: 4.7.0(vite@6.4.3) + version: 4.7.0(supports-color@10.2.2)(vite@6.4.3) eslint: specifier: ^9.28.0 - version: 9.39.5 + version: 9.39.5(supports-color@10.2.2) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.5) + version: 6.10.2(eslint@9.39.5(supports-color@10.2.2)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.39.5) + version: 5.2.0(eslint@9.39.5(supports-color@10.2.2)) globals: specifier: ^16.2.0 version: 16.5.0 jsdom: specifier: ^26.1.0 - version: 26.1.0 + version: 26.1.0(supports-color@10.2.2) openapi-typescript: specifier: ^7.8.0 version: 7.13.0(typescript@5.8.3) @@ -83,13 +89,13 @@ importers: version: 5.8.3 typescript-eslint: specifier: ^8.33.0 - version: 8.70.0(eslint@9.39.5)(typescript@5.8.3) + version: 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) vite: specifier: ^6.3.5 version: 6.4.3 vitest: specifier: ^3.2.0 - version: 3.2.7(jsdom@26.1.0) + version: 3.2.7(jsdom@26.1.0(supports-color@10.2.2))(supports-color@10.2.2) packages: @@ -408,6 +414,12 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fontsource/funnel-sans@5.3.0': + resolution: {integrity: sha512-LmINq/6qfoYW1LOkOwaiZw7YVEpi3QACa36V7jxgBlO8MPGPbeSxFrPjk4lF6uP1ZbCp2VsBWmneo+1b59GbbA==} + + '@fontsource/geist-mono@5.3.0': + resolution: {integrity: sha512-UtJ1BBBCVpMYdIcW7nEB45UAoAw5M53ZXs2t0ciPW+IokuAAIc56M8+kW5tXbRJCTpDw4XTtU7proT6NdQAHTg==} + '@hookform/resolvers@5.9.1': resolution: {integrity: sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==} peerDependencies: @@ -2195,16 +2207,16 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 @@ -2233,19 +2245,19 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -2266,14 +2278,14 @@ snapshots: dependencies: '@babel/types': 7.29.8 - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 '@babel/runtime@7.29.7': {} @@ -2284,7 +2296,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - '@babel/traverse@7.29.8': + '@babel/traverse@7.29.8(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 @@ -2399,14 +2411,14 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)': + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(supports-color@10.2.2))': dependencies: - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.2': + '@eslint/config-array@0.21.2(supports-color@10.2.2)': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3(supports-color@10.2.2) @@ -2422,7 +2434,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.7': + '@eslint/eslintrc@3.3.7(supports-color@10.2.2)': dependencies: ajv: 6.15.0 debug: 4.4.3(supports-color@10.2.2) @@ -2445,6 +2457,10 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fontsource/funnel-sans@5.3.0': {} + + '@fontsource/geist-mono@5.3.0': {} + '@hookform/resolvers@5.9.1(react-hook-form@7.87.0(react@19.3.0))(zod@3.25.76)': dependencies: '@standard-schema/utils': 0.3.0 @@ -2676,15 +2692,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3))(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.70.0(eslint@9.39.5)(typescript@5.8.3) + '@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.70.0 - '@typescript-eslint/type-utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3) - '@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3) + '@typescript-eslint/type-utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.70.0 - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) ignore: 7.0.9 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.8.3) @@ -2692,19 +2708,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3)': + '@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.70.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.70.0(supports-color@10.2.2)(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.8.3) '@typescript-eslint/types': 8.70.0 @@ -2722,13 +2738,13 @@ snapshots: dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.70.0(eslint@9.39.5)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -2736,9 +2752,9 @@ snapshots: '@typescript-eslint/types@8.70.0': {} - '@typescript-eslint/typescript-estree@8.70.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.70.0(supports-color@10.2.2)(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.70.0(typescript@5.8.3) + '@typescript-eslint/project-service': 8.70.0(supports-color@10.2.2)(typescript@5.8.3) '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@5.8.3) '@typescript-eslint/types': 8.70.0 '@typescript-eslint/visitor-keys': 8.70.0 @@ -2751,13 +2767,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.70.0(eslint@9.39.5)(typescript@5.8.3)': + '@typescript-eslint/utils@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.70.0 '@typescript-eslint/types': 8.70.0 - '@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3) - eslint: 9.39.5 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3) + eslint: 9.39.5(supports-color@10.2.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -2767,11 +2783,11 @@ snapshots: '@typescript-eslint/types': 8.70.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@4.7.0(vite@6.4.3)': + '@vitejs/plugin-react@4.7.0(supports-color@10.2.2)(vite@6.4.3)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 @@ -3198,7 +3214,7 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(supports-color@10.2.2)): dependencies: aria-query: 5.3.2 array-includes: 3.2.0 @@ -3208,7 +3224,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -3217,9 +3233,9 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.5): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.5(supports-color@10.2.2)): dependencies: - eslint: 9.39.5 + eslint: 9.39.5(supports-color@10.2.2) eslint-scope@8.4.0: dependencies: @@ -3232,14 +3248,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.5: + eslint@9.39.5(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 + '@eslint/config-array': 0.21.2(supports-color@10.2.2) '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.7 + '@eslint/eslintrc': 3.3.7(supports-color@10.2.2) '@eslint/js': 9.39.5 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.8 @@ -3417,7 +3433,7 @@ snapshots: dependencies: void-elements: 3.1.0 - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@10.2.2): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@10.2.2) @@ -3584,13 +3600,13 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: + jsdom@26.1.0(supports-color@10.2.2): dependencies: cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2(supports-color@10.2.2) https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.27 @@ -4122,13 +4138,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.70.0(eslint@9.39.5)(typescript@5.8.3): + typescript-eslint@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5)(typescript@5.8.3))(eslint@9.39.5)(typescript@5.8.3) - '@typescript-eslint/parser': 8.70.0(eslint@9.39.5)(typescript@5.8.3) - '@typescript-eslint/typescript-estree': 8.70.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.70.0(eslint@9.39.5)(typescript@5.8.3) - eslint: 9.39.5 + '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3))(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) + '@typescript-eslint/parser': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@10.2.2)(typescript@5.8.3) + '@typescript-eslint/utils': 8.70.0(eslint@9.39.5(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.8.3) + eslint: 9.39.5(supports-color@10.2.2) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -4154,7 +4170,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.2.4: + vite-node@3.2.4(supports-color@10.2.2): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@10.2.2) @@ -4186,7 +4202,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitest@3.2.7(jsdom@26.1.0): + vitest@3.2.7(jsdom@26.1.0(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 @@ -4209,10 +4225,10 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 2.0.0 vite: 6.4.3 - vite-node: 3.2.4 + vite-node: 3.2.4(supports-color@10.2.2) why-is-node-running: 2.3.0 optionalDependencies: - jsdom: 26.1.0 + jsdom: 26.1.0(supports-color@10.2.2) transitivePeerDependencies: - jiti - less diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 18f4b2a..1336976 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -25,6 +25,9 @@ const ImportsPage = lazy(() => const LoginPage = lazy(() => import('@/pages/LoginPage').then((module) => ({ default: module.LoginPage })), ) +const OverviewPage = lazy(() => + import('@/pages/OverviewPage').then((module) => ({ default: module.OverviewPage })), +) const NotFoundPage = lazy(() => import('@/pages/NotFoundPage').then((module) => ({ default: module.NotFoundPage })), ) @@ -72,7 +75,7 @@ export function AppRoutes() { } /> }> }> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx index 6a2b92d..747802b 100644 --- a/frontend/src/components/layout/AppLayout.tsx +++ b/frontend/src/components/layout/AppLayout.tsx @@ -1,67 +1,97 @@ -import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom' +import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/Button' import { LangSwitcher } from '@/components/layout/LangSwitcher' import { useLogout, useMe } from '@/hooks/useAuth' const NAV_ITEMS = [ - { to: '/accounts', key: 'nav.accounts' }, - { to: '/budget', key: 'nav.budget' }, - { to: '/operations', key: 'nav.operations' }, - { to: '/review', key: 'nav.review' }, - { to: '/imports', key: 'nav.imports' }, - { to: '/analytics', key: 'nav.analytics' }, - { to: '/settings', key: 'nav.settings' }, + { to: '/', key: 'nav.month', end: true }, + { to: '/budget', key: 'nav.budget', end: false }, + { to: '/operations', key: 'nav.operations', end: false }, + { to: '/imports', key: 'nav.imports', end: false }, + { to: '/review', key: 'nav.review', end: false }, + { to: '/analytics', key: 'nav.analytics', end: false }, + { to: '/settings', key: 'nav.settings', end: false }, + { to: '/accounts', key: 'nav.accounts', end: false }, ] as const +const MOBILE_NAV_ITEMS = NAV_ITEMS.filter((item) => + ['/', '/operations', '/imports', '/analytics', '/settings'].includes(item.to), +) + export function AppLayout() { const { t } = useTranslation() const me = useMe() const logout = useLogout() const navigate = useNavigate() + const location = useLocation() + const pageKey = location.pathname.split('/').filter(Boolean)[0] ?? 'overview' + + const handleLogout = () => + logout.mutate(undefined, { + onSettled: () => navigate('/login', { replace: true }), + }) return (
{t('common.skipToContent')} -
- -
{children}
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 6a0ccfc..2da93a0 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1,18 +1,56 @@ { "app": { - "title": "Grossbook — finance tracker", - "brand": "Grossbook", - "footer": "Closed beta · RUB · zero-based budget" + "title": "Finspace — household finance", + "brand": "Finspace", + "footer": "RUB · private household ledger", + "financialSpace": "Financial space", + "ownerActive": "Owner · active", + "synced": "All data synced" }, "nav": { "label": "Primary navigation", + "month": "This month", "accounts": "Accounts", "budget": "Budget", - "operations": "Operations", + "operations": "Ledger", "review": "Review queue", - "imports": "Imports", + "imports": "Statements", "analytics": "Analytics", - "settings": "Settings & data" + "settings": "Space & data" + }, + "overview": { + "subtitle": "Your household money, explained for the current month.", + "openBudget": "Open full budget", + "newOperation": "New operation", + "availableNow": "cash available now", + "afterPlan": "after plan and expected income", + "reserved": "reserved into categories", + "ofFunded": "spent from funded money", + "monthStory": "How the month is moving", + "liveState": "Live state", + "poolAndIncome": "Pool and actual income", + "funded": "Funded to categories", + "spent": "Spent from categories", + "poolRemaining": "Pool remaining", + "closeReadiness": "Close readiness", + "blockers": "blockers", + "readyToClose": "No blockers. This month is ready to close.", + "importsNeedReview": "{{count}} statement needs review", + "reviewBlockers": "Review blockers", + "categoryPulse": "Category pulse" + }, + "authShell": { + "statement": "Your household ledger, sealed by design.", + "explanation": "Invite-only access. Every account, operation and statement stays inside one encrypted financial space.", + "session": "14-day secure session", + "encryption": "AES-GCM statement encryption", + "isolation": "Space-isolated financial data", + "private": "Private workspace", + "singleUse": "Single-use invitations", + "rub": "RUB accounting", + "signIn": "Access / sign in", + "invitation": "Access / invitation", + "reset": "Access / reset password" }, "lang": { "label": "Language" @@ -108,8 +146,8 @@ "archiveConfirmBody": "Account “{{name}}” will leave working lists but remain in history." }, "operations": { - "title": "Operations", - "subtitle": "Expenses, income, transfers and refunds.", + "title": "Ledger", + "subtitle": "Every expense, income, transfer and refund in one traceable sequence.", "new": "New operation", "empty": "No operations match the selected filters.", "search": "Search", @@ -149,8 +187,8 @@ "nextPage": "Next" }, "budget": { - "title": "Budget", - "subtitle": "Month plan, funding and the general pool.", + "title": "Monthly plan", + "subtitle": "Plan every ruble, then fund the month from the real pool.", "monthPicker": "Month picker", "month": "Month", "prevMonth": "Previous month", @@ -291,8 +329,8 @@ "revertBody": "Operations created by the “{{fileName}}” import will be removed atomically; affected months will require reopening." }, "analytics": { - "title": "Analytics", - "subtitle": "Four fixed reports for your space.", + "title": "Financial picture", + "subtitle": "Spending, plan accuracy, cash flow and balances without noise.", "range": "Period:", "months_one": "{{count}} month", "months_other": "{{count}} months", @@ -314,8 +352,8 @@ "unallocated": "Unallocated" }, "settings": { - "title": "Settings & data", - "subtitle": "Profile, export and space data controls.", + "title": "Space & data", + "subtitle": "Identity, language, export and lifecycle controls for this financial space.", "profile": "Profile", "timezone": "Time zone", "dataTitle": "Data", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index 4c4503e..db2605f 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -1,18 +1,56 @@ { "app": { - "title": "Гроссбух — финансовый трекер", - "brand": "Гроссбух", - "footer": "Закрытая бета · RUB · zero-based бюджет" + "title": "Finspace — семейные финансы", + "brand": "Finspace", + "footer": "RUB · приватный семейный гроссбух", + "financialSpace": "Финансовое пространство", + "ownerActive": "Владелец · активно", + "synced": "Все данные синхронизированы" }, "nav": { "label": "Основная навигация", + "month": "Этот месяц", "accounts": "Счета", "budget": "Бюджет", - "operations": "Операции", + "operations": "Гроссбух", "review": "Очередь разбора", - "imports": "Импорт", + "imports": "Выписки", "analytics": "Аналитика", - "settings": "Настройки и данные" + "settings": "Пространство и данные" + }, + "overview": { + "subtitle": "Семейные деньги с объяснением текущего месяца.", + "openBudget": "Открыть весь бюджет", + "newOperation": "Новая операция", + "availableNow": "деньги, доступные сейчас", + "afterPlan": "после плана и ожидаемых доходов", + "reserved": "зарезервировано по категориям", + "ofFunded": "потрачено из профинансированного", + "monthStory": "Как движется месяц", + "liveState": "Текущее состояние", + "poolAndIncome": "Пул и полученный доход", + "funded": "Распределено по категориям", + "spent": "Потрачено по категориям", + "poolRemaining": "Осталось в пуле", + "closeReadiness": "Готовность к закрытию", + "blockers": "блокировок", + "readyToClose": "Блокировок нет. Месяц можно закрывать.", + "importsNeedReview": "Выписок на проверке: {{count}}", + "reviewBlockers": "Разобрать блокировки", + "categoryPulse": "Состояние категорий" + }, + "authShell": { + "statement": "Семейный гроссбух, закрытый по замыслу.", + "explanation": "Доступ только по приглашению. Счета, операции и выписки остаются внутри одного зашифрованного пространства.", + "session": "Защищённая сессия на 14 дней", + "encryption": "Шифрование выписок AES-GCM", + "isolation": "Изоляция данных пространства", + "private": "Приватное пространство", + "singleUse": "Одноразовые приглашения", + "rub": "Учёт в рублях", + "signIn": "Доступ / вход", + "invitation": "Доступ / приглашение", + "reset": "Доступ / сброс пароля" }, "lang": { "label": "Язык" @@ -108,8 +146,8 @@ "archiveConfirmBody": "Счёт «{{name}}» скроется из рабочих списков, но останется в истории." }, "operations": { - "title": "Операции", - "subtitle": "Расходы, доходы, переводы и возвраты.", + "title": "Гроссбух", + "subtitle": "Расходы, доходы, переводы и возвраты в одной проверяемой последовательности.", "new": "Новая операция", "empty": "Операций по выбранным условиям не найдено.", "search": "Поиск", @@ -149,8 +187,8 @@ "nextPage": "Дальше" }, "budget": { - "title": "Бюджет", - "subtitle": "План месяца, обеспечение и общий пул.", + "title": "План месяца", + "subtitle": "Спланируйте каждый рубль и обеспечьте месяц из реального пула.", "monthPicker": "Выбор месяца", "month": "Месяц", "prevMonth": "Предыдущий месяц", @@ -291,8 +329,8 @@ "revertBody": "Операции, созданные импортом «{{fileName}}», будут удалены атомарно; затронутые месяцы потребуют переоткрытия." }, "analytics": { - "title": "Аналитика", - "subtitle": "Четыре фиксированных отчёта по вашему пространству.", + "title": "Финансовая картина", + "subtitle": "Расходы, точность плана, денежный поток и балансы без шума.", "range": "Период:", "months_one": "{{count}} месяц", "months_few": "{{count}} месяца", @@ -316,8 +354,8 @@ "unallocated": "Без распределения" }, "settings": { - "title": "Настройки и данные", - "subtitle": "Профиль, экспорт и управление данными пространства.", + "title": "Пространство и данные", + "subtitle": "Профиль, язык, экспорт и жизненный цикл финансового пространства.", "profile": "Профиль", "timezone": "Часовой пояс", "dataTitle": "Данные", diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9606c07..e4067f8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,13 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import '@fontsource/funnel-sans/400.css' +import '@fontsource/funnel-sans/500.css' +import '@fontsource/funnel-sans/600.css' +import '@fontsource/funnel-sans/700.css' +import '@fontsource/geist-mono/400.css' +import '@fontsource/geist-mono/500.css' +import '@fontsource/geist-mono/600.css' +import '@fontsource/geist-mono/700.css' import App from './App' import './i18n' import './styles/global.css' diff --git a/frontend/src/pages/AcceptInvitePage.tsx b/frontend/src/pages/AcceptInvitePage.tsx index 66590c2..54a52bf 100644 --- a/frontend/src/pages/AcceptInvitePage.tsx +++ b/frontend/src/pages/AcceptInvitePage.tsx @@ -6,7 +6,7 @@ import { Link, Navigate, useNavigate, useSearchParams } from 'react-router-dom' import { z } from 'zod' import { Button } from '@/components/ui/Button' import { TextField } from '@/components/ui/fields' -import { LangSwitcher } from '@/components/layout/LangSwitcher' +import { AuthLayout } from '@/components/layout/AuthLayout' import { useAcceptInvite, useMe } from '@/hooks/useAuth' import { describeApiError } from '@/lib/apiErrors' @@ -63,17 +63,11 @@ export function AcceptInvitePage() { }) return ( -
-
-
- - - -
-

{t('auth.inviteTitle')}

-

{t('auth.inviteSubtitle')}

+ {apiError ? (
{apiError} @@ -114,7 +108,6 @@ export function AcceptInvitePage() {

{t('auth.backToLogin')}

-
-
+ ) } diff --git a/frontend/src/pages/ImportDetailPage.tsx b/frontend/src/pages/ImportDetailPage.tsx index d9e977c..3fc183b 100644 --- a/frontend/src/pages/ImportDetailPage.tsx +++ b/frontend/src/pages/ImportDetailPage.tsx @@ -246,22 +246,22 @@ export function ImportDetailPage() { {data.rows.map((row) => ( - - + + {formatDate(row.occurredOn, locale)} - + {row.description ?? '—'} {row.payeeHint ? · {row.payeeHint} : null} {row.direction === 'in' ? '+' : '−'} {formatMoney(row.amountKopecks, locale)} - + {row.warnings.length === 0 ? ( '—' ) : ( @@ -274,7 +274,7 @@ export function ImportDetailPage() { )} - +