HomeBrew Apartments
Client-only browser for 100 short-term rentals across Madrid, Berlin and Paris, with debounced search, a saved list and host publishing.

Problem
Apartment listing sites are often cluttered with ads and slow search. I wanted a fast, focused browsing experience for a single listing source.
Approach
A single-page React app with no server, no database and no API: the
catalogue of 100 listings ships with the app as a JSON file, and anything
you save or publish is stored in your browser’s localStorage. That
constraint shapes most of the design. You search by city or country, open a
listing for its photos, facts, price breakdown and host, save the ones you
like, and can publish a listing of your own into the same grid.
Architecture
- State:
App.jsxis the single source of truth. There is no context and no store — state is passed down as props, deliberately, for an app this size - The catalogue loads through a
useCataloguehook that dynamically imports the listing JSON, keeping it out of the initial chunk - Search results are derived, never stored: they are recomputed from the full listing set plus a debounced query on every render
- Persistence: saved listings are ids in
localStorage, with the listing objects resolved from the full catalogue; host-created listings are prepended to it - Styling: CSS Modules per component over a
tokens.cssdesign system — no raw hex values or magic pixel numbers outside it
Key decisions
- Deriving results rather than storing them fixed a real bug: an earlier
version kept results in their own state and they drifted from the search
input. For the same reason, lookups go against the full catalogue and
never against the filtered view, which resolves to
undefinedfor anything the current search excludes ApartmentCardis memoized and takes a plain boolean rather than anisFavoritepredicate, whose identity changes on every toggle and would re-render all 100 cards- The catalogue is generated at build time from the original data dump, dropping the roughly 60% of fields nothing renders — the generated file is committed so a fresh clone runs immediately
- The display serif carries the wordmark and every number — prices, ratings, city counts — while UI text uses the system stack
Results
A responsive browser over 100 listings with case-insensitive, debounced city and country search, a live city ledger above the grid that doubles as filter chips and the reset control, a saved list and published listings that both survive a reload, and light and dark themes following the system preference until you choose one. A Vitest suite covers the defects that actually occurred, and lint runs at zero tolerated warnings.
What I’d do differently
I’d paginate or virtualize the grid — client-side filtering over a much
larger dataset than this one would need it. The bigger limitation is
localStorage: saved and published listings never leave the browser they
were created in, so a real backend is the next step rather than an
optimization.