From 3586b2ee5529c8a43d8589f5481640634bf3e7eb Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 12:50:04 +0000 Subject: [PATCH 1/6] feat: Initialize Frontend Project (Vite + React + TypeScript) Run: c91683d9-8f4e-4816-938c-936a82e798c9 Task: ff9df6f3-76a8-418a-b7d2-c96d7320edfe Agent: builder --- RUNNING.md | 75 +++- frontend/index.html | 14 + frontend/package.json | 30 ++ frontend/public/logo.svg | 21 ++ frontend/src/App.tsx | 33 ++ frontend/src/__tests__/App.test.tsx | 56 +++ frontend/src/__tests__/ContactInfo.test.tsx | 109 ++++++ frontend/src/__tests__/RecentSales.test.tsx | 64 ++++ frontend/src/components/CompanyName.tsx | 14 + frontend/src/components/ContactInfo.tsx | 184 +++++++++ frontend/src/components/Logo.tsx | 19 + frontend/src/components/Profile.tsx | 39 ++ frontend/src/components/RecentSales.tsx | 80 ++++ frontend/src/main.tsx | 16 + frontend/src/styles/global.css | 396 ++++++++++++++++++++ frontend/src/test-setup.ts | 1 + frontend/src/vite-env.d.ts | 2 + frontend/tsconfig.json | 22 ++ frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 20 + 20 files changed, 1187 insertions(+), 18 deletions(-) create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/logo.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/__tests__/App.test.tsx create mode 100644 frontend/src/__tests__/ContactInfo.test.tsx create mode 100644 frontend/src/__tests__/RecentSales.test.tsx create mode 100644 frontend/src/components/CompanyName.tsx create mode 100644 frontend/src/components/ContactInfo.tsx create mode 100644 frontend/src/components/Logo.tsx create mode 100644 frontend/src/components/Profile.tsx create mode 100644 frontend/src/components/RecentSales.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles/global.css create mode 100644 frontend/src/test-setup.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/RUNNING.md b/RUNNING.md index 77896cf..34fa3ca 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,72 @@ -# Running the Todo API +# Madhuri Real Estate — Frontend -## Prerequisites +## TEAM_BRIEF +stack: TypeScript/React+Vite +test_runner: cd frontend && npx vitest run +lint_tool: none +coverage_tool: none +coverage_threshold: 0 +coverage_applies: false -- Python 3.10 or later +## Overview +Single-page real estate website for Madhuri Real Estate built with Vite, React, and TypeScript. -## Install dependencies +## Prerequisites +- Node.js >= 18 +- npm >= 9 +## Setup ```bash -pip install fastapi uvicorn pydantic +cd frontend +npm install ``` -For running the test suite you will also need: - +## Development ```bash -pip install httpx pytest +cd frontend +npm run dev ``` +The app will start at http://localhost:3000. -## Start the server - +## Build ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +cd frontend +npm run build ``` +Output goes to `frontend/dist/`. -The API will be available at . - -Interactive docs are served at . - -## Run the tests - +## Testing ```bash -pytest tests/ +cd frontend +npm test +``` +Runs all tests under `src/__tests__/` via Vitest + jsdom. + +## Project Structure +``` +frontend/ +├── public/ +│ └── logo.svg +├── src/ +│ ├── __tests__/ +│ │ ├── App.test.tsx +│ │ ├── ContactInfo.test.tsx +│ │ └── RecentSales.test.tsx +│ ├── components/ +│ │ ├── CompanyName.tsx +│ │ ├── ContactInfo.tsx +│ │ ├── Logo.tsx +│ │ ├── Profile.tsx +│ │ └── RecentSales.tsx +│ ├── styles/ +│ │ └── global.css +│ ├── App.tsx +│ ├── main.tsx +│ ├── test-setup.ts +│ └── vite-env.d.ts +├── index.html +├── package.json +├── tsconfig.json +├── tsconfig.node.json +└── vite.config.ts ``` 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} + + )} +
+ +
+ +