This is a solution to the Personal finance app challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.
Users should be able to:
- See all of the personal finance app data at-a-glance on the overview page
- View all transactions on the transactions page with pagination for every ten transactions
- Search, sort, and filter transactions
- Create, read, update, delete (CRUD) budgets and saving pots
- View the latest three transactions for each budget category created
- View progress towards each pot
- Add money to and withdraw money from pots
- View recurring bills and the status of each for the current month
- Search and sort recurring bills
- Receive validation messages if required form fields aren't completed
- Navigate the whole app and perform all actions using only their keyboard
- View the optimal layout for the interface depending on their device's screen size
- See hover and focus states for all interactive elements on the page
- Bonus: Save details to a database (built as a full-stack app with a Rails API)
- Bonus: Create an account and log in (JWT authentication with per-user data isolation)
- Solution URL: https://github.com/natashapl/personal-finance
- Live Site URL: https://personal-finance-natashasworld.vercel.app
Frontend
- Angular 21 — standalone components, no NgModules
- Angular Signals for all state management (
signal(),computed(),effect()) ChangeDetectionStrategy.OnPushthroughout, with zoneless change detection (no Zone.js)- Reactive Forms for form handling
- Angular
NgOptimizedImagefor all static images - CSS custom properties driven by the Figma design system
- Mobile-first responsive layout using CSS Grid and Flexbox
- Public Sans variable font with
rel="preload"
Backend
- Ruby on Rails 8.1 in API-only mode
- SQLite via Active Record
- Puma web server
bcryptfor password hashing,jwtgem for token issuancerack-corsfor cross-origin requests- Per-user data scoping — all budgets, pots, and transactions belong to a user
- Demo account with read-only enforcement (
is_demoflag +require_non_demo!guard)
Testing
- Playwright for end-to-end testing — 25 tests, all passing
- E2E tests cover: auth flows, budgets CRUD, pots CRUD (including add/withdraw money), transactions CRUD, search/filter, and pagination
- Vitest for unit testing — 80 tests, all passing
- Unit tests cover:
parseServerErrorsutility (all error formats),ToastService(show/dismiss/auto-dismiss/announce),RecurringBills(ordinal formatting, currency formatting, computed totals),Transactions(pagination window logic, form validation, date formatting),Pots(progress calculations, withdrawal validation, form validation)
Quality
- Lighthouse scores (desktop): 100 Accessibility · 100 Best Practices · 100 SEO · 100 Performance
- Lighthouse scores (mobile): 100 Accessibility · 100 Best Practices · 100 SEO · 94 Performance
- WCAG AA compliant: focus management, colour contrast, ARIA roles, keyboard navigation
- Custom
FocusTrapDirectivelocks Tab/Shift+Tab inside modals and restores focus on close aria-busyon all page roots,aria-liveloading announcements,role="progressbar"on savings bars
Angular Signals + Zoneless
Migrating to zoneless change detection (provideZonelessChangeDetection(), no zone.js import) gave a measurable Lighthouse performance improvement because Zone.js patches dozens of browser APIs at startup. Since signals already notify Angular when state changes, Zone.js adds overhead without any benefit in a fully signal-based app.
JWT Auth in a SPA
Storing the JWT in localStorage and attaching it via an HTTP interceptor keeps authentication simple without cookies or sessions. The interceptor pattern means no component ever touches the token directly:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};Playwright strict mode pitfalls Two recurring issues when writing the E2E suite:
-
getByRole('button', { name: 'Withdraw' })matched both the Withdraw action button and the ellipsis button whosearia-labelwas"Options for Withdraw Pot"— because Playwright's default is a case-insensitive substring match. Fix:{ exact: true }. -
getByLabel('Password')matched both the password input and the show/hide toggle button (aria-label"Show password"). Fix:{ exact: true }.
Multi-toast assertions in Playwright
When two success toasts are visible simultaneously, expect(locator).toContainText('x') requires all matched elements to contain the text — so a "Budget added" toast would cause the assertion for "Budget updated" to fail. Fix: .filter({ hasText: '...' }).toBeVisible() to scope to the specific toast.
This project was built in collaboration with Claude (Anthropic) using the Claude Code CLI.
How AI was used:
- Scaffolding the Rails API (models, controllers, auth, routing, CORS) and the Angular application structure from scratch
- Designing and implementing the accessibility layer (focus trap directive, ARIA attributes, live regions)
- Writing the full Playwright E2E test suite and iteratively debugging failures
- Writing the Vitest unit test suite covering pure utility functions, service state management, and component logic
What worked well: Claude was effective at diagnosing issues as they came up, especially Playwright failures. Claude was also efficient at following instructions and requirements.
What required iteration: Some Playwright fixes needed multiple rounds because the failure mode wasn't visible without running the tests. Providing the actual Playwright error output (including the page snapshot) dramatically sped up diagnosis compared to describing the failure in prose.
- Frontend Mentor - @natashapl
