diff --git a/QA.md b/QA.md new file mode 100644 index 0000000..154a4b7 --- /dev/null +++ b/QA.md @@ -0,0 +1,19 @@ +app_type: spa +coverage_applies: false +coverage_source: null +coverage_threshold: 0 +coverage_tool: none +install_steps: +- npm install +- npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event + jsdom vitest @vitest/coverage-v8 +lint_tool: none +notes: Verify that all main components render and user interactions work as expected + in the real estate SPA. +stack: TypeScript/React+Vite +test_files: +- src/__tests__/App.test.tsx +- src/__tests__/RecentSales.test.tsx +- src/__tests__/ContactInfo.test.tsx +test_runner: npx vitest run +workspace: /tmp/forge-repos/website-for-realestate-single-page-with--c91683d9 diff --git a/RUNNING.md b/RUNNING.md index 77896cf..a6b6b42 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,48 @@ -# Running the Todo API +# Madhuri Real Estate - Single Page Application -## Prerequisites +## TEAM_BRIEF +stack: TypeScript/React +test_runner: npx jest --verbose +lint_tool: none +coverage_tool: none +coverage_threshold: 0 +coverage_applies: false -- Python 3.10 or later +## Overview +A React-based single-page application for Madhuri Real Estate, featuring a logo, company name, agent profile, recent sales showcase, and contact form. -## Install dependencies +## Prerequisites +- Node.js 18+ +- npm +## Setup ```bash -pip install fastapi uvicorn pydantic +npm install ``` -For running the test suite you will also need: - +## Running Tests ```bash -pip install httpx pytest +npm test ``` -## Start the server - -```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +## Project Structure ``` - -The API will be available at . - -Interactive docs are served at . - -## Run the tests - -```bash -pytest tests/ +src/ +├── App.tsx # Main application component +├── components/ +│ ├── Logo.tsx # Logo display component +│ ├── CompanyName.tsx # Company name heading component +│ ├── Profile.tsx # Agent profile component +│ ├── RecentSales.tsx # Recent sales grid component +│ └── ContactInfo.tsx # Contact info and form component +├── styles/ +│ └── global.css # Global styles +├── __tests__/ +│ ├── App.test.tsx # App composition tests +│ ├── RecentSales.test.tsx # Recent sales rendering tests +│ └── ContactInfo.test.tsx # Contact form validation tests +└── __mocks__/ + └── fileMock.ts # Static file mock for Jest +public/ +└── logo.svg # Company logo asset ``` diff --git a/SETUP.md b/SETUP.md index 643c59c..c69790a 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,25 +1,20 @@ # Setup Instructions -## Install Dependencies +The following files are generated by tooling and should NOT be hand-written: -```bash -pip install -r requirements.txt -``` - -## Install Test Dependencies - -```bash -pip install pytest httpx -``` +- `package-lock.json` — generated by `npm install` +- `node_modules/` — generated by `npm install` +- `build/` — generated by `npm run build` -## Run Tests +## Initial Setup ```bash -pytest tests/ -v -``` +# Install all dependencies (generates package-lock.json and node_modules/) +npm install -## Run the Application +# Verify the setup by running tests +npm test -```bash -uvicorn main:app --reload +# Start development server +npm run dev ``` diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ba0de3f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Madhuri Real Estate + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a3499c9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "madhuri-real-estate", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src/" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.1", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "jsdom": "^23.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vitest": "^1.1.0" + } +} diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..dc3bf45 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + M + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..d1c285c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Logo } from './components/Logo'; +import { CompanyName } from './components/CompanyName'; +import { Profile } from './components/Profile'; +import { RecentSales } from './components/RecentSales'; +import { ContactInfo } from './components/ContactInfo'; + +/** + * Main application component. + * Composes all page sections in order: Logo, CompanyName, Profile, RecentSales, ContactInfo. + */ +const App: React.FC = () => { + return ( +
+
+ + +
+ +
+ + + +
+ +
+

© {new Date().getFullYear()} Madhuri Real Estate. All rights reserved.

+
+
+ ); +}; + +export default App; diff --git a/frontend/src/__tests__/App.test.tsx b/frontend/src/__tests__/App.test.tsx new file mode 100644 index 0000000..2236bc8 --- /dev/null +++ b/frontend/src/__tests__/App.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import App from '../App'; + +describe('App', () => { + it('renders the Logo component', () => { + render(); + expect(screen.getByTestId('logo')).toBeInTheDocument(); + }); + + it('renders the CompanyName component', () => { + render(); + expect(screen.getByTestId('company-name')).toBeInTheDocument(); + }); + + it('renders the Profile component', () => { + render(); + expect(screen.getByTestId('profile')).toBeInTheDocument(); + }); + + it('renders the RecentSales component', () => { + render(); + expect(screen.getByTestId('recent-sales')).toBeInTheDocument(); + }); + + it('renders the ContactInfo component', () => { + render(); + expect(screen.getByTestId('contact-info')).toBeInTheDocument(); + }); + + it('renders all sections in correct order', () => { + const { container } = render(); + + const header = container.querySelector('.app-header'); + const main = container.querySelector('.app-main'); + + expect(header).toBeInTheDocument(); + expect(main).toBeInTheDocument(); + + // Logo and CompanyName should be in the header + expect(header!.querySelector('[data-testid="logo"]')).toBeInTheDocument(); + expect(header!.querySelector('[data-testid="company-name"]')).toBeInTheDocument(); + + // Profile, RecentSales, ContactInfo should be in main + const mainChildren = main!.children; + expect(mainChildren[0]).toHaveAttribute('data-testid', 'profile'); + expect(mainChildren[1]).toHaveAttribute('data-testid', 'recent-sales'); + expect(mainChildren[2]).toHaveAttribute('data-testid', 'contact-info'); + }); + + it('renders the footer with copyright', () => { + render(); + const year = new Date().getFullYear().toString(); + expect(screen.getByText(new RegExp(year))).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/ContactInfo.test.tsx b/frontend/src/__tests__/ContactInfo.test.tsx new file mode 100644 index 0000000..392a35d --- /dev/null +++ b/frontend/src/__tests__/ContactInfo.test.tsx @@ -0,0 +1,109 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect } from 'vitest'; +import { ContactInfo } from '../components/ContactInfo'; + +describe('ContactInfo', () => { + it('renders the section heading', () => { + render(); + expect(screen.getByText('Contact Us')).toBeInTheDocument(); + }); + + it('displays phone number', () => { + render(); + expect(screen.getByTestId('contact-phone')).toHaveTextContent('(555) 123-4567'); + }); + + it('displays email address', () => { + render(); + expect(screen.getByTestId('contact-email')).toHaveTextContent( + 'info@madhurirealestate.com', + ); + }); + + it('displays physical address', () => { + render(); + expect(screen.getByTestId('contact-address')).toHaveTextContent( + '100 Main Street, Suite 200, Springfield, IL 62701', + ); + }); + + it('renders the contact form', () => { + render(); + expect(screen.getByTestId('contact-form')).toBeInTheDocument(); + }); + + it('renders name, email, and message fields', () => { + render(); + expect(screen.getByLabelText(/name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/message/i)).toBeInTheDocument(); + }); + + it('shows validation errors when submitting empty form', async () => { + const user = userEvent.setup(); + render(); + + const submitBtn = screen.getByRole('button', { name: /send message/i }); + await user.click(submitBtn); + + expect(screen.getByTestId('error-name')).toHaveTextContent('Name is required'); + expect(screen.getByTestId('error-email')).toHaveTextContent('Email is required'); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'Message is required', + ); + }); + + it('shows email validation error for invalid email', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByLabelText(/name/i), 'John Doe'); + await user.type(screen.getByLabelText(/email/i), 'invalid-email'); + await user.type(screen.getByLabelText(/message/i), 'Hello there'); + + const submitBtn = screen.getByRole('button', { name: /send message/i }); + await user.click(submitBtn); + + expect(screen.getByTestId('error-email')).toHaveTextContent( + 'Please enter a valid email address', + ); + }); + + it('shows success message on valid form submission', async () => { + const user = userEvent.setup(); + render(); + + await user.type(screen.getByLabelText(/name/i), 'John Doe'); + await user.type(screen.getByLabelText(/email/i), 'john@example.com'); + await user.type(screen.getByLabelText(/message/i), 'I am interested in a property'); + + const submitBtn = screen.getByRole('button', { name: /send message/i }); + await user.click(submitBtn); + + expect(screen.getByTestId('form-success')).toBeInTheDocument(); + expect( + screen.getByText(/thank you for your message/i), + ).toBeInTheDocument(); + }); + + it('clears form fields after successful submission', async () => { + const user = userEvent.setup(); + render(); + + const nameInput = screen.getByLabelText(/name/i) as HTMLInputElement; + const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement; + const messageInput = screen.getByLabelText(/message/i) as HTMLTextAreaElement; + + await user.type(nameInput, 'John Doe'); + await user.type(emailInput, 'john@example.com'); + await user.type(messageInput, 'Hello'); + + const submitBtn = screen.getByRole('button', { name: /send message/i }); + await user.click(submitBtn); + + expect(nameInput.value).toBe(''); + expect(emailInput.value).toBe(''); + expect(messageInput.value).toBe(''); + }); +}); diff --git a/frontend/src/__tests__/RecentSales.test.tsx b/frontend/src/__tests__/RecentSales.test.tsx new file mode 100644 index 0000000..6a71b9b --- /dev/null +++ b/frontend/src/__tests__/RecentSales.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { RecentSales, Sale } from '../components/RecentSales'; + +const mockSales: Sale[] = [ + { + id: 1, + address: '100 Test Lane, Testville', + price: '$500,000', + imageUrl: '/logo.svg', + soldDate: '2024-02-01', + }, + { + id: 2, + address: '200 Sample Road, Mocktown', + price: '$750,000', + imageUrl: '/logo.svg', + soldDate: '2024-01-20', + }, +]; + +describe('RecentSales', () => { + it('renders the section heading', () => { + render(); + expect(screen.getByText('Recent Sales')).toBeInTheDocument(); + }); + + it('renders all sale cards when sales data is provided', () => { + render(); + const cards = screen.getAllByTestId('sale-card'); + expect(cards).toHaveLength(2); + }); + + it('renders sale addresses', () => { + render(); + expect(screen.getByText('100 Test Lane, Testville')).toBeInTheDocument(); + expect(screen.getByText('200 Sample Road, Mocktown')).toBeInTheDocument(); + }); + + it('renders sale prices', () => { + render(); + expect(screen.getByText('$500,000')).toBeInTheDocument(); + expect(screen.getByText('$750,000')).toBeInTheDocument(); + }); + + it('renders empty state when sales is an empty array', () => { + render(); + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + expect( + screen.getByText('No recent sales to display at this time.'), + ).toBeInTheDocument(); + }); + + it('renders empty state when sales is null', () => { + render(); + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + }); + + it('renders default sales when no sales prop is provided', () => { + render(); + const cards = screen.getAllByTestId('sale-card'); + expect(cards.length).toBeGreaterThan(0); + }); +}); diff --git a/frontend/src/components/CompanyName.tsx b/frontend/src/components/CompanyName.tsx new file mode 100644 index 0000000..94e577a --- /dev/null +++ b/frontend/src/components/CompanyName.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +/** + * CompanyName component. + * Showcases the company name with distinctive, brand-aligned typography. + */ +export const CompanyName: React.FC = () => { + return ( +
+

Madhuri Real Estate

+

Your Trusted Partner in Property

+
+ ); +}; diff --git a/frontend/src/components/ContactInfo.tsx b/frontend/src/components/ContactInfo.tsx new file mode 100644 index 0000000..9a363c3 --- /dev/null +++ b/frontend/src/components/ContactInfo.tsx @@ -0,0 +1,184 @@ +import React, { useState } from 'react'; + +/** Shape of the contact form data. */ +interface ContactFormData { + name: string; + email: string; + message: string; +} + +/** Shape of form validation errors. */ +interface FormErrors { + name?: string; + email?: string; + message?: string; +} + +/** + * ContactInfo component. + * Displays contact information (phone, email, address) and a contact form + * with client-side validation for required fields. + */ +export const ContactInfo: React.FC = () => { + const [formData, setFormData] = useState({ + name: '', + email: '', + message: '', + }); + + const [errors, setErrors] = useState({}); + const [submitted, setSubmitted] = useState(false); + + /** Validate form fields and return errors object. */ + const validate = (data: ContactFormData): FormErrors => { + const newErrors: FormErrors = {}; + + if (!data.name.trim()) { + newErrors.name = 'Name is required'; + } + + if (!data.email.trim()) { + newErrors.email = 'Email is required'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { + newErrors.email = 'Please enter a valid email address'; + } + + if (!data.message.trim()) { + newErrors.message = 'Message is required'; + } + + return newErrors; + }; + + /** Handle input changes. */ + const handleChange = ( + e: React.ChangeEvent, + ): void => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + + // Clear the error for this field when user starts typing + if (errors[name as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [name]: undefined })); + } + }; + + /** Handle form submission. */ + const handleSubmit = (e: React.FormEvent): void => { + e.preventDefault(); + const validationErrors = validate(formData); + + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + + setSubmitted(true); + setFormData({ name: '', email: '', message: '' }); + setErrors({}); + }; + + return ( +
+

Contact Us

+ +
+
+ + +
+ Address: + + 100 Main Street, Suite 200, Springfield, IL 62701 + +
+
+ +
+ {submitted && ( +
+ Thank you for your message! We will get back to you soon. +
+ )} + +
+ + + {errors.name && ( + + {errors.name} + + )} +
+ +
+ + + {errors.email && ( + + {errors.email} + + )} +
+ +
+ +