Skip to content
Open
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
87 changes: 87 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Real Estate Website — Architecture Document

## Project Overview

A modern real estate listing website built with React, TypeScript, Vite, and Tailwind CSS.

## Project Structure

```
src/
├── types/
│ └── models.ts # TypeScript interfaces & type aliases
├── data/
│ ├── mockProperties.ts # 12+ mock property listings
│ ├── mockAgents.ts # 3+ mock real estate agents
│ └── mockNeighborhoods.ts # 6 mock neighborhoods
├── components/
│ ├── atoms/ # Buttons, badges, inputs, icons
│ ├── molecules/ # Cards, forms, nav items
│ └── organisms/ # Header, footer, property grid, hero
├── pages/
│ ├── HomePage.tsx
│ ├── PropertiesPage.tsx
│ ├── PropertyDetailPage.tsx
│ ├── AgentsPage.tsx
│ ├── AgentDetailPage.tsx
│ ├── NeighborhoodsPage.tsx
│ ├── NeighborhoodDetailPage.tsx
│ └── ContactPage.tsx
├── hooks/ # Custom React hooks
├── utils/ # Formatting helpers (price, address)
├── App.tsx # Root component with React Router
├── main.tsx # Entry point
└── index.css # Tailwind directives & custom styles
```

## Data Models

All domain models live in `src/types/models.ts`:

| Model | Purpose |
| ---------------- | --------------------------------------------- |
| `Property` | A real estate listing with images, price, etc. |
| `Agent` | A real estate agent / broker |
| `Neighborhood` | A geographic area with aggregate stats |
| `ContactFormData`| Shape of the contact form submission |

Supporting union types: `PropertyType`, `PropertyStatus`, `PreferredContact`.

## Routing (React Router v6)

| Path | Page |
| ----------------------------- | ------------------------ |
| `/` | HomePage |
| `/properties` | PropertiesPage |
| `/properties/:slug` | PropertyDetailPage |
| `/agents` | AgentsPage |
| `/agents/:id` | AgentDetailPage |
| `/neighborhoods` | NeighborhoodsPage |
| `/neighborhoods/:slug` | NeighborhoodDetailPage |
| `/contact` | ContactPage |

## Mock Data Strategy

- All images use real **Unsplash** photo IDs with the format:
`https://images.unsplash.com/photo-{ID}?w={W}&h={H}&fit=crop`
- Properties: exterior & interior photography
- Agents: professional headshot portraits
- Neighborhoods: aerial / cityscape / street-level imagery

## Styling

- **Tailwind CSS** utility-first approach
- Design tokens defined via `tailwind.config.js` `extend` section
- Colour palette: slate (neutrals), blue (primary), amber (accent)
- Typography: Inter (sans), Georgia fallback (serif headings)
- Responsive breakpoints: `sm` 640px, `md` 768px, `lg` 1024px, `xl` 1280px

## Build Tooling

| Tool | Purpose |
| ----------- | ---------------------- |
| Vite | Dev server & bundler |
| TypeScript | Static type checking |
| ESLint | Linting |
| Prettier | Code formatting |
| Vitest | Unit / integration tests |
59 changes: 59 additions & 0 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Running the Application

This document describes how to install dependencies and run the real estate website locally.

## Prerequisites

- **Node.js** >= 18.x
- **npm** >= 9.x (ships with Node.js 18+)

## Installation

```bash
npm install
```

## Development Server

Start the Vite development server with hot module replacement:

```bash
npm run dev
```

The app will be available at [http://localhost:5173](http://localhost:5173) by default.

## Production Build

Create an optimised production build:

```bash
npm run build
```

The output will be placed in the `dist/` directory.

## Preview Production Build

Serve the production build locally for testing:

```bash
npm run preview
```

## Linting

Run ESLint to check for code quality issues:

```bash
npm run lint
```

## Routes

| Path | Page | Description |
| ------------------ | ------------------- | ---------------------------- |
| `/` | HomePage | Property listings & hero |
| `/property/:id` | PropertyDetailPage | Individual property details |
| `/contact` | ContactPage | Contact form |
| `*` (catch-all) | NotFoundPage | Styled 404 page |
36 changes: 36 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Setup Instructions

## Prerequisites

- Node.js >= 18
- npm >= 9 (or pnpm / yarn)

## Install Dependencies

```bash
npm install
```

## Run Tests

```bash
npx vitest run
```

## Run Tests in Watch Mode

```bash
npx vitest
```

## Project Configuration

The following lock files and generated directories are **not** checked into version control
and must be produced by the toolchain:

- `node_modules/`
- `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`
- `dist/`
- `.vite/`

Run `npm install` to generate them locally.
126 changes: 126 additions & 0 deletions frontend/src/components/AgentCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import AgentCard from './AgentCard';
import type { Agent } from '../types/models';

const mockAgent: Agent = {
id: 'agent-1',
name: 'Jane Doe',
title: 'Senior Real Estate Agent',
phone: '(555) 123-4567',
email: 'jane.doe@realty.com',
photo: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=400&fit=crop',
bio: 'With over 15 years of experience in luxury real estate, Jane specializes in helping families find their dream homes in the greater metropolitan area.',
specialties: ['Luxury Homes', 'Waterfront Properties'],
propertiesCount: 42,
rating: 4.9,
socialLinks: {
linkedin: 'https://linkedin.com/in/janedoe',
},
};

describe('AgentCard', () => {
it('renders without crashing', () => {
const onContact = vi.fn();
render(<AgentCard agent={mockAgent} onContact={onContact} />);
expect(screen.getByTestId('agent-card')).toBeInTheDocument();
});

it('displays the agent name', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
});

it('displays the agent title', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
expect(screen.getByText('Senior Real Estate Agent')).toBeInTheDocument();
});

it('displays the agent phone number', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const phoneLinks = screen.getAllByTestId('agent-phone');
expect(phoneLinks.length).toBeGreaterThan(0);
expect(phoneLinks[0]).toHaveTextContent('(555) 123-4567');
});

it('displays the agent email', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const emailLinks = screen.getAllByTestId('agent-email');
expect(emailLinks.length).toBeGreaterThan(0);
expect(emailLinks[0]).toHaveTextContent('jane.doe@realty.com');
});

it('displays the agent bio', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
expect(screen.getByText(/With over 15 years/)).toBeInTheDocument();
});

it('renders the agent headshot with correct src and alt', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const img = screen.getByAlt('Jane Doe headshot') as HTMLImageElement;
expect(img).toBeInTheDocument();
expect(img.src).toBe(mockAgent.photo);
});

it('calls onContact with the agent when button is clicked', () => {
const onContact = vi.fn();
render(<AgentCard agent={mockAgent} onContact={onContact} />);
fireEvent.click(screen.getByTestId('contact-button'));
expect(onContact).toHaveBeenCalledTimes(1);
expect(onContact).toHaveBeenCalledWith(mockAgent);
});

it('renders the Contact Agent button text', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
expect(screen.getByTestId('contact-button')).toHaveTextContent('Contact Agent');
});

it('renders in vertical layout by default', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const card = screen.getByTestId('agent-card');
expect(card.className).toContain('text-center');
});

it('renders in horizontal layout when specified', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} layout="horizontal" />);
const card = screen.getByTestId('agent-card');
expect(card.className).toContain('flex-row');
expect(card.className).not.toContain('text-center');
});

it('applies custom className', () => {
render(
<AgentCard agent={mockAgent} onContact={vi.fn()} className="my-custom-class" />
);
const card = screen.getByTestId('agent-card');
expect(card.className).toContain('my-custom-class');
});

it('renders phone link with tel: href', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const phoneLinks = screen.getAllByTestId('agent-phone');
expect(phoneLinks[0].closest('a')).toHaveAttribute('href', 'tel:(555) 123-4567');
});

it('renders email link with mailto: href', () => {
render(<AgentCard agent={mockAgent} onContact={vi.fn()} />);
const emailLinks = screen.getAllByTestId('agent-email');
expect(emailLinks[0].closest('a')).toHaveAttribute('href', 'mailto:jane.doe@realty.com');
});

it('renders correctly with a different agent', () => {
const otherAgent: Agent = {
...mockAgent,
id: 'agent-2',
name: 'John Smith',
title: 'Broker Associate',
phone: '(555) 987-6543',
email: 'john.smith@realty.com',
bio: 'John brings a decade of market expertise.',
};
render(<AgentCard agent={otherAgent} onContact={vi.fn()} />);
expect(screen.getByText('John Smith')).toBeInTheDocument();
expect(screen.getByText('Broker Associate')).toBeInTheDocument();
expect(screen.getByText(/John brings a decade/)).toBeInTheDocument();
});
});
Loading