Full design · Shops
Real estate agency
A complete site and back office for a boutique residential agency, sales and leasing.
Live preview. Click around, it works.
About this template
A complete site and back office for a boutique residential agency, sales and leasing. Buyers and renters search by suburb, price, beds, baths, parking and type, sort, and see results on a drawn suburb map; each listing has a full screen gallery, a floor plan drawn from room data, inspection times to register for and add to a calendar (.ics), an enquiry form and a rough repayment guide clearly labelled as general information. Saved homes, side by side compare, an inspection run sheet and search alerts that fire when an agent lists a match. Sellers request an appraisal in three steps. The agent side has a drag and drop pipeline from appraisal to sold, today's inspections with attendee check in and CSV export, an enquiries inbox with reply templates, a listing editor with photo ordering and map placement, performance charts, and settings that change what buyers see.
What the agents are asked
The first message sent when you use this template. You can edit the plan before anything is built.
Adapt this real estate agency app to my business. Keep the buyer and renter side (Home, Listings with filters, sorting and the drawn suburb map, Listing with the gallery viewer, floor plan, inspections with calendar files, enquiry form and the rough repayment guide, Saved with alerts, Sell with us with the appraisal request) and the agent side (Pipeline, Inspections with check in, Enquiries with reply templates, Listing editor, Performance, Settings). Change the agency name, colours, office details, team, suburbs and the drawn map, the listings, photographs and reply templates to match my agency, and keep the repayment guide and any stamp duty figure labelled as general information, not advice. Where I want real accounts, emailed enquiries and alerts, or uploaded photographs, wire in authentication, the database in schema.sql, an email service and storage as the README describes, never putting a secret key in the browser.
A site and back office for a boutique residential agency that sells and leases homes. Buyers and renters search listings by suburb, price, bedrooms, bathrooms, parking and type, sort them, and see them on a drawn suburb map. Each listing has a gallery with a full screen viewer, a floor plan drawn from room data, inspection times they can register for and add to a calendar, an enquiry form, and a rough repayment guide labelled as general information. They can save homes, compare them, plan a Saturday of inspections and set alerts. Sellers request an appraisal. The agent side runs the pipeline, the day's inspections with check in, an enquiries inbox with reply templates, a listing editor, performance charts and settings.
"Lintel Residential" is an invented business. The agency, team, suburbs, streets, listings, prices, buyers and figures are made up for the template. Phone numbers come from the ranges ACMA sets aside for fiction, and email addresses use example domains. The suburbs and the map are fictional on purpose, so no real address appears. Rename everything freely.
Files
| File | What it is |
|---|---|
index.html | Shell: demo ribbon with the Viewing as switch, top bar, agent sidebar, main, footer, phone tab bar, a <dialog> for sheets, a <dialog> for the photo viewer, a toast region, and one <script type="module">. |
styles.css | Tokens first (light, dark by prefers-color-scheme, and a manual override on <html data-theme>), then components, motion, responsive rules and a print stylesheet. Every colour is a token. |
script.js | One plain ES module, no dependencies. Sections: helpers, icons, constants (suburbs, photographs, floor plans), seed data, store, rpc, chrome, buyer screens, agent screens, router, events. |
schema.sql | Postgres tables, row level security, grants, the public functions, triggers that write notifications, seed rows and a VERIFY block. |
template.json | Catalogue entry, with the identity (fonts, colours, the motion moment). |
media.json | The photographs and the hero loop, with the prompts they were made from (see Art direction). |
There is no build step. Serve the folder with any static server that also serves /template-media/real-estate/.
Roles and the Viewing as switch
The ribbon says Demo and Sample data, and carries Viewing as: Buyer or renter, Agent. As Agent, a second control picks who you are signed in as (reply sign offs and "my" listings use it). Opening an agent page as a buyer shows a page explaining who it is for. In the live app the switch is replaced by sign in: staff are the users with an agents row (see is_staff() in schema.sql).
Screens
| Route | Screen |
|---|---|
#/ | Home. An asymmetric hero: headline, buy or rent search and live counts on the left, the facade loop on the right with the listing it belongs to. A three question welcome for a first visit (buy or rent, suburbs, bedrooms, and an optional alert) that opens the search with those filters. New listings in an uneven grid. The five suburbs beside a sticky street photograph, with sample medians and station walks. Saturday's open inspections as a run sheet with one calendar file for all. The team. A band leading to Sell with us. |
#/listings | Listings. Buy or rent. Search text (press /), suburbs, type, price from and to, beds, baths, cars, include under offer, and sort (newest, price both ways, largest land, next inspection). Everything lives in the URL, so a search can be shared or saved. Active filters show as removable chips. The drawn map shows a price pin per result; pressing a suburb on the map filters by it, pins and cards highlight each other, and a pin opens a small card. First load shows a loading state; an empty result suggests which one filter to loosen and how many homes that would bring back. Save search turns the filters into an alert. |
#/listing/:id | Listing. Photo grid (a swipe strip on a phone) opening a full screen viewer: arrow keys, Home and End, swipe, thumbnails, Esc. Price, key facts, description, features, the floor plan drawn from PLANS with a measurements switch, a small map, and the rough guide: for a sale, price, deposit, rate and term give a monthly repayment, the loan, cash needed up front and a transfer duty line from a sample scale; for a lease, monthly rent, bond and move in cost. Both say plainly they are general information, not advice. The sticky side card has the agent, inspection times with Register and Add to calendar (one .ics per time or all at once), Save and Share, and the enquiry form with topics. |
#/saved | Saved and alerts, four tabs. Saved homes with a private note each, Remove with Undo, and Compare up to three side by side. Inspection plan: every upcoming time for saved or registered homes grouped by day, clashes marked, one calendar file for all. Alerts: frequency, on or off, see matches, delete with Undo, and the notifications feed (a daily summary waits until delivered). Enquiries: what you asked and the agent's replies. |
#/sell | Sell with us. How we sell in four steps beside a sticky photograph, recent sales with days on market, and a three step appraisal request with validation at each step. The request lands in the agent Pipeline. |
#/agent | Pipeline. Four figures, then Appraisal, Listed, Under offer and Sold or leased. Drag a card between stages or use its Move menu. Moving to Listed checks the listing can go live and sends matching buyer alerts; moving to Sold asks for the price and date; every move has Undo. Appraisal requests from the site sit in the first column with Book visit, Start listing and Archive. Filter by agent. |
#/agent/inspections | Inspections. A week strip and day navigation. Each inspection shows registered and checked in counts, a searchable attendee list with one tap check in (and undo), walk ins added and checked in at once (a phone number can be required in Settings), CSV export, thank you notes for everyone who came (demo send), and Cancel with Undo. Schedule a new inspection from here or from the editor. |
#/agent/enquiries | Enquiries. New, Replied, Closed and All, with search. The thread shows the question, the listing and the replies. Reply templates fill in the first name, address, price, next inspection and phone, and Settings adds the sign off. Close, reopen, delete with Undo. Keys: j and k move, r replies, e closes. |
#/agent/listing/:id | Listing editor. Sale or lease, address, suburb, type, rooms, land and inside area, the price as shown and the numbers search uses, headline, description, features, photographs (drag to reorder, or Alt with the arrow keys, Make cover, Remove with Undo, add from the library), floor plan, the map pin placed by pressing the map (the suburb follows the pin), agent and inspections. A live preview card, a checklist of what is needed to go live, Save, Save and publish, Take offline, Delete with Undo. Leaving with unsaved changes asks first. #/agent/listing/new starts a blank one. |
#/agent/performance | Performance. 14, 30 or 90 days. Views, enquiries, average days to sell and inspection attendance. Views a day (line with a crosshair tooltip, keyboard arrows move it, and a table view), enquiries a day, days on market by listing (finished campaigns filled, running ones outlined), and a sortable table from views to enquiries with CSV export. |
#/agent/settings | Settings. Agency details and licence number (shown in the footer), show price guides publicly (off turns every sale into Contact agent and hides sold prices), how long the New badge lasts, the repayment guide's starting rate and deposit, requiring a phone at check in, alerts as a listing goes live or in a daily summary, the reply sign off, the team, and data: remove or restore sample data, export everything as JSON, reset the demo. |
Also: notifications (the bell), display preferences (theme, reduce motion, map beside results, show the welcome again), keyboard shortcuts (?), and a print stylesheet so a listing prints cleanly.
On a phone the bottom tab bar holds each side's screens; from 860 px the buyer side uses the top navigation and from 1024 px the agent side uses the sidebar.
Data model
store in script.js holds the same twelve tables as schema.sql, with the same field names. Screens read through store and change data through rpc, whose functions are named after the SQL that does the same job:
rpc in the browser | On the server |
|---|---|
register_for_inspection | public.register_for_inspection(), security definer, validates and de-duplicates by email |
submit_enquiry | public.submit_enquiry(), security definer, with a flood limit, counts the enquiry |
request_appraisal | public.request_appraisal(), security definer |
record_listing_view | public.record_listing_view(), counts published listings only |
toggle_saved | insert or delete on saved_properties (own rows only); a trigger counts saves |
reply_to_enquiry | insert on enquiry_replies (staff only); the notify_reply trigger tells the buyer |
move_listing_stage | public.move_listing_stage(), runs as the caller so row level security decides; the notify_matches trigger sends alerts |
check_in, add_walk_in | update or insert on inspection_attendees (staff only) |
Counters (listing_stats) are only ever incremented in SQL (on conflict ... do update set views = views + 1), never read, changed and written back.
Sample rows carry is_sample. Remove sample data deletes them in one step (with Undo) and keeps anything you added; Restore sample data puts them back beside it.
Sample data is also versioned. SAMPLE_VERSION in script.js is stored alongside the saved data; when it does not match what is on the visitor's device, reseedSampleData() swaps every remaining is_sample row for a freshly generated one (new photo references, dates relative to today) on the next load, while every row a visitor added or edited themselves passes through untouched. A business that has already removed the sample data never has it reappear this way. Bump SAMPLE_VERSION whenever seedData() changes.
Swapping localStorage for Supabase
Only store and rpc change. For example:
import { createClient } from '@supabase/supabase-js';
const db = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // the anon key is public by design; never the service key
// store.all('listings') becomes:
const { data: listings } = await db.from('listings').select('*').order('listed_on', { ascending: false });
// rpc.submit_enquiry(input) becomes:
const { data: id, error } = await db.rpc('submit_enquiry', {
p_listing: input.listing_id, p_name: input.name, p_email: input.email,
p_phone: input.phone, p_topic: input.topic, p_message: input.message,
});The screens already render from whatever store returns, so making the reads async and awaiting them in render() is the only structural change.
Where the server work goes
- Email (enquiry copies to the agent, replies to the buyer, alert emails, the 7am daily summary, inspection change notices and thank you notes): a server function reading
notificationsandinspection_attendeeswith the service role, sending through an email service. The key lives in project settings, never in the browser. Every place this happens in the demo says "Demo" and "Connect an email service". - Photographs: upload to storage (a
listing-photosbucket, staff write, public read) and store the file names inlistings.photos, in order. The demo editor picks from the template's own photographs instead. - Price guides when hidden: with Show price guides off, the page says Contact agent but
price_lowandprice_highare still readable by anyone through the API. If the numbers themselves must stay private, move them to a staff only table and search through a function.
The rough guides
The repayment guide uses the standard principal and interest formula at one fixed rate, with no fees. The transfer duty line uses DUTY_SCALE in script.js: a simplified, made up marginal scale shaped like the states' scales, not any state's real one, and before concessions. Both are labelled "A rough guide, not advice" and "General information only" on the page. Replace the scale with your state's published rates if you want a closer figure, and keep the labels.
Art direction
Quiet and specific, like a well kept agency window: large photographs at golden hour, a serif that reads like a brass nameplate, and a lot of room.
Palette. Three solid colours as tokens at the top of styles.css: Oxblood #7A2631 (actions, price pins, the lintel in the wordmark, the one accent), Ink #191718 (text, the ribbon, the footer, the agent sidebar and the sell band) and Chalk #F3F1ED (the ground, a cool stone white rather than cream). Dark mode swaps Ink and Chalk and lifts Oxblood to #A33D4A for buttons and #E7A3AB for text. No gradients.
Type. Gloock for display: one weight, upright, high contrast, used large with tight tracking for headlines, addresses, prices and figures, and never in italic. Schibsted Grotesk for text and the interface, with tabular figures for prices and counts. Scale about 1.33, body line height 1.6.
Layout. Photographs are square cornered; controls use a 3 px radius and chips are pills. Home opens on an asymmetric hero (copy on five columns, the loop on seven), then an uneven listings grid, a sticky street photograph beside the suburbs, a run sheet, staggered portraits and a full bleed ink band with the kitchen. Listing pages open on a photo grid across the width, then a reading column with a sticky side card. The agent side is dense and calm: ink sidebar, chalk working surface, the same type.
Motion. One job each, all behind prefers-reduced-motion and the Reduce motion preference:
- The orchestrated moment, "Open for inspection": open a listing and its card photograph carries into the gallery's lead frame (
document.startViewTransitionwith a sharedview-transition-name); the badges, address, suburb, price and facts then step in one after another; when the floor plan scrolls into view its outer walls draw, then each room's walls, then the room names and measurements fade in as each room closes, and the north arrow last (stroke-dashoffsetonpathLength="1"paths, about 1.6 s, once). - Home: a staggered entrance on first load, once, with the hero photograph settling from a slight scale.
- Scroll: text rises in with
animation-timeline: view()inside@supports; otherwise an IntersectionObserver adds a class at threshold 0.2. - Photographs: a
clip-path: inset()reveal with a slight scale, once. - Sticky photograph columns on Home (the street) and Sell with us (the bedroom), and a sticky side card on listings.
- Screen changes cross fade with
document.startViewTransitionwhere supported. - Hovers 150 to 200 ms, eased, no bounce.
- The hero loop plays only when motion is allowed, only while on screen and only while the tab is visible, and has a pause button. With reduced motion it is not rendered and the photograph shows.
Photographs. Generated from media.json in one shoot: late afternoon golden hour, low sun raking in from the right, warm 3800K highlights with cool neutral shadows, a 35mm feel at f/4, fine grain, natural skin with no airbrushing, no text, signs or house numbers. Served from public/template-media/real-estate/ and referenced root relative as /template-media/real-estate/<name>; project creation makes those paths absolute. Every frame has a fallback colour (--ph) so the layout holds while it loads.
| File | Size | Where it is used |
|---|---|---|
facade.webp | 1600 x 893 | Home hero (poster and still for the loop), cover of 14 Quandong Street, editor library |
facade-loop.mp4 | 1120 wide, 5 s, H.264, muted | Home hero loop, decorative (aria-hidden); the photograph beneath keeps the alt text |
living.webp | 1600 x 1067 | Listing galleries, cover of 5 Wirra Close, editor library |
kitchen.webp | 1000 x 1241 | Home sell band, listing galleries, cover of 19 Linden Avenue |
bedroom.webp | 1200 x 908 | Sell with us sticky column, listing galleries, cover of 12/40 Seabright Parade |
courtyard.webp | 1000 x 1321 | Sell with us hero, cover of 2/9 Corella Mews and a sold listing |
street.webp | 1600 x 818 | Home suburbs sticky column, cover of the terraces |
apartment.webp | 1000 x 1500 | Cover of the apartments |
portrait-principal.webp, portrait-agent.webp | 700 x 700 | Team section, agent cards, inbox and pipeline |
The sample listings share these nine photographs, so the same room appears in more than one home; a real agency replaces them with each listing's own. The property manager has no photograph on purpose, to show the monogram a new team member gets until one is added. Which photograph belongs where is PHOTO and each listing's photos in script.js.
Honesty
No reviews or testimonials are shown, because none can be backed. Suburb medians, sold results and performance history are marked Sample. The repayment and move in figures are general information, not financial advice, and say so where they appear. Demo sends (emails, alerts, thank you notes) say Demo.