Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm test
- run: pnpm build
189 changes: 153 additions & 36 deletions src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,103 @@ import {
createRootRoute,
createRoute,
createRouter,
lazyRouteComponent,
Outlet,
redirect,
} from '@tanstack/react-router';
import { RootLayout } from './layout/RootLayout';
import { OverviewPage } from '@/routes/overview/OverviewPage';
import { TrackPage } from '@/routes/track/TrackPage';
import { TrackMonthPage } from '@/routes/track/TrackMonthPage';
import { TrackAddPage } from '@/routes/track/TrackAddPage';
import { TrackTransactionPage } from '@/routes/track/TrackTransactionPage';
import { TrackCategoriesPage } from '@/routes/track/TrackCategoriesPage';
import { TrackBudgetPage } from '@/routes/track/TrackBudgetPage';
import { TrackRecurringPage } from '@/routes/track/TrackRecurringPage';
import { SplitPage } from '@/routes/split/SplitPage';
import { SplitGroupPage } from '@/routes/split/SplitGroupPage';
import { SplitGroupAddPage } from '@/routes/split/SplitGroupAddPage';
import { SplitGroupBalancesPage } from '@/routes/split/SplitGroupBalancesPage';
import { SplitGroupActivityPage } from '@/routes/split/SplitGroupActivityPage';
import { SplitGroupSettlePage } from '@/routes/split/SplitGroupSettlePage';
import { SplitGroupSettingsPage } from '@/routes/split/SplitGroupSettingsPage';
import { LendPage } from '@/routes/lend/LendPage';
import { LendPersonPage } from '@/routes/lend/LendPersonPage';
import { LendLedgerPage } from '@/routes/lend/LendLedgerPage';
import { LendAddPage } from '@/routes/lend/LendAddPage';
import { SettingsPage } from '@/routes/settings/SettingsPage';
import { PeoplePage } from '@/routes/settings/PeoplePage';
import { BackupPage } from '@/routes/settings/BackupPage';
import { SearchPage } from '@/routes/overview/SearchPage';
import { OnboardingPage } from '@/routes/OnboardingPage';

const OverviewPage = lazyRouteComponent(
() => import('@/routes/overview/OverviewPage'),
'OverviewPage',
);
const SearchPage = lazyRouteComponent(
() => import('@/routes/overview/SearchPage'),
'SearchPage',
);
const OnboardingPage = lazyRouteComponent(
() => import('@/routes/OnboardingPage'),
'OnboardingPage',
);

const TrackPage = lazyRouteComponent(() => import('@/routes/track/TrackPage'), 'TrackPage');
const TrackMonthPage = lazyRouteComponent(
() => import('@/routes/track/TrackMonthPage'),
'TrackMonthPage',
);
const TrackAddPage = lazyRouteComponent(
() => import('@/routes/track/TrackAddPage'),
'TrackAddPage',
);
const TrackTransactionPage = lazyRouteComponent(
() => import('@/routes/track/TrackTransactionPage'),
'TrackTransactionPage',
);
const TrackCategoriesPage = lazyRouteComponent(
() => import('@/routes/track/TrackCategoriesPage'),
'TrackCategoriesPage',
);
const TrackBudgetPage = lazyRouteComponent(
() => import('@/routes/track/TrackBudgetPage'),
'TrackBudgetPage',
);
const TrackRecurringPage = lazyRouteComponent(
() => import('@/routes/track/TrackRecurringPage'),
'TrackRecurringPage',
);

const SplitPage = lazyRouteComponent(() => import('@/routes/split/SplitPage'), 'SplitPage');
const SplitGroupPage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupPage'),
'SplitGroupPage',
);
const SplitGroupAddPage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupAddPage'),
'SplitGroupAddPage',
);
const SplitGroupBalancesPage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupBalancesPage'),
'SplitGroupBalancesPage',
);
const SplitGroupActivityPage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupActivityPage'),
'SplitGroupActivityPage',
);
const SplitGroupSettlePage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupSettlePage'),
'SplitGroupSettlePage',
);
const SplitGroupSettingsPage = lazyRouteComponent(
() => import('@/routes/split/SplitGroupSettingsPage'),
'SplitGroupSettingsPage',
);

const LendPage = lazyRouteComponent(() => import('@/routes/lend/LendPage'), 'LendPage');
const LendPersonPage = lazyRouteComponent(
() => import('@/routes/lend/LendPersonPage'),
'LendPersonPage',
);
const LendLedgerPage = lazyRouteComponent(
() => import('@/routes/lend/LendLedgerPage'),
'LendLedgerPage',
);
const LendAddPage = lazyRouteComponent(
() => import('@/routes/lend/LendAddPage'),
'LendAddPage',
);

const SettingsPage = lazyRouteComponent(
() => import('@/routes/settings/SettingsPage'),
'SettingsPage',
);
const PeoplePage = lazyRouteComponent(
() => import('@/routes/settings/PeoplePage'),
'PeoplePage',
);
const BackupPage = lazyRouteComponent(
() => import('@/routes/settings/BackupPage'),
'BackupPage',
);

const rootRoute = createRootRoute({
component: () => (
Expand All @@ -62,17 +131,37 @@ const indexRoute = createRoute({
},
});

const overviewRoute = createRoute({ getParentRoute: () => rootRoute, path: '/overview', component: OverviewPage });
const searchRoute = createRoute({ getParentRoute: () => rootRoute, path: '/search', component: SearchPage });
const onboardingRoute = createRoute({ getParentRoute: () => rootRoute, path: '/onboarding', component: OnboardingPage });
const overviewRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/overview',
component: OverviewPage,
});
const searchRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/search',
component: SearchPage,
});
const onboardingRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/onboarding',
component: OnboardingPage,
});

const trackRoute = createRoute({ getParentRoute: () => rootRoute, path: '/track', component: TrackPage });
const trackRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track',
component: TrackPage,
});
const trackMonthRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track/month/$year/$month',
component: TrackMonthPage,
});
const trackAddRoute = createRoute({ getParentRoute: () => rootRoute, path: '/track/add', component: TrackAddPage });
const trackAddRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track/add',
component: TrackAddPage,
});
const trackTransactionRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track/transaction/$transactionId',
Expand All @@ -83,14 +172,22 @@ const trackCategoriesRoute = createRoute({
path: '/track/categories',
component: TrackCategoriesPage,
});
const trackBudgetRoute = createRoute({ getParentRoute: () => rootRoute, path: '/track/budget', component: TrackBudgetPage });
const trackBudgetRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track/budget',
component: TrackBudgetPage,
});
const trackRecurringRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/track/recurring',
component: TrackRecurringPage,
});

const splitRoute = createRoute({ getParentRoute: () => rootRoute, path: '/split', component: SplitPage });
const splitRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/split',
component: SplitPage,
});
const splitGroupRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/split/group/$groupId',
Expand Down Expand Up @@ -122,7 +219,11 @@ const splitGroupSettingsRoute = createRoute({
component: SplitGroupSettingsPage,
});

const lendRoute = createRoute({ getParentRoute: () => rootRoute, path: '/lend', component: LendPage });
const lendRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/lend',
component: LendPage,
});
const lendPersonRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/lend/person/$personId',
Expand All @@ -133,11 +234,27 @@ const lendLedgerRoute = createRoute({
path: '/lend/ledger/$ledgerId',
component: LendLedgerPage,
});
const lendAddRoute = createRoute({ getParentRoute: () => rootRoute, path: '/lend/add', component: LendAddPage });
const lendAddRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/lend/add',
component: LendAddPage,
});

const settingsRoute = createRoute({ getParentRoute: () => rootRoute, path: '/settings', component: SettingsPage });
const peopleRoute = createRoute({ getParentRoute: () => rootRoute, path: '/settings/people', component: PeoplePage });
const backupRoute = createRoute({ getParentRoute: () => rootRoute, path: '/settings/backup', component: BackupPage });
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/settings',
component: SettingsPage,
});
const peopleRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/settings/people',
component: PeoplePage,
});
const backupRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/settings/backup',
component: BackupPage,
});

const routeTree = rootRoute.addChildren([
indexRoute,
Expand Down
14 changes: 14 additions & 0 deletions src/components/ui/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import clsx from 'clsx';

export function Spinner({ className }: { className?: string }) {
return (
<div
className={clsx(
'inline-block h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-brand-600',
className,
)}
role="status"
aria-label="Loading"
/>
);
}
19 changes: 5 additions & 14 deletions src/components/ui/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import clsx from 'clsx';

export function Spinner({ className }: { className?: string }) {
return (
<div
className={clsx(
'inline-block h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-brand-600',
className,
)}
role="status"
aria-label="Loading"
/>
);
}
/**
* Compatibility re-export for older direct imports.
* New code should import Spinner from `./Spinner` or the UI barrel.
*/
export { Spinner } from './Spinner';
2 changes: 1 addition & 1 deletion src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export { MoneyInput } from './MoneyInput';
export { DateInput } from './DateInput';
export { CategoryPicker, PaymentMethodPicker, CurrencyPicker } from './Picker';
export { Toggle } from './Toggle';
export { Spinner } from './Tabs';
export { Spinner } from './Spinner';
export { ToastProvider, useToast } from './Toast';
export { CelebrationProvider, useCelebration } from './Celebration';
export { Modal } from './Modal';
6 changes: 3 additions & 3 deletions src/db/repositories/base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Base repository helpers.
*
* Repositories are the only place that touch Dexie. Components
* and services call `xxxRepository.create()` etc. They never
* reach into `db.table.add(...)` directly.
* Repositories own persistence writes. Query/projection modules may read
* Dexie directly when they need reactive or aggregate data, but UI components
* should not operate on database tables themselves.
*
* Every write:
* - sets `createdAt` / `updatedAt`
Expand Down
Loading
Loading