.',
+ );
+}
+
+ReactDOM.createRoot(rootElement).render(
+
+
+ ,
+);
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..9461c56
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,65 @@
+/**
+ * Shared TypeScript interfaces for the Maddie Real Estate landing page.
+ */
+
+/** Represents a navigation link item. */
+export interface NavLink {
+ /** Display label for the link. */
+ label: string;
+ /** Target section ID (without the '#' prefix). */
+ href: string;
+}
+
+/** Represents a property listing card. */
+export interface Property {
+ /** Unique identifier for the property. */
+ id: string;
+ /** Street address of the property. */
+ address: string;
+ /** Sale price formatted as a display string (e.g. "$1,250,000"). */
+ price: string;
+ /** Number of bedrooms. */
+ bedrooms: number;
+ /** Number of bathrooms. */
+ bathrooms: number;
+ /** Square footage of the property. */
+ sqft: number;
+ /** URL to the property image. */
+ imageUrl: string;
+ /** Status of the listing. */
+ status: 'sold' | 'active' | 'pending';
+}
+
+/** Represents the agent's profile information. */
+export interface AgentProfile {
+ /** Agent's full name. */
+ name: string;
+ /** Professional title or designation. */
+ title: string;
+ /** Short biography paragraph. */
+ bio: string;
+ /** URL to the agent's headshot photo. */
+ imageUrl: string;
+ /** Phone number for contact. */
+ phone: string;
+ /** Email address for contact. */
+ email: string;
+}
+
+/** Props interface for the PropertyCard component. */
+export interface PropertyCardProps {
+ /** The property data to display. */
+ property: Property;
+}
+
+/** Contact form field values. */
+export interface ContactFormData {
+ /** Sender's full name. */
+ name: string;
+ /** Sender's email address. */
+ email: string;
+ /** Sender's phone number (optional). */
+ phone?: string;
+ /** Message body. */
+ message: string;
+}
diff --git a/src/utils/scrollTo.ts b/src/utils/scrollTo.ts
new file mode 100644
index 0000000..6bac0c5
--- /dev/null
+++ b/src/utils/scrollTo.ts
@@ -0,0 +1,20 @@
+/**
+ * Smoothly scroll to an element identified by its DOM id.
+ *
+ * @param elementId - The id attribute of the target element (without '#').
+ * @param offset - Optional vertical offset in pixels (e.g. for fixed headers).
+ */
+export function scrollToElement(elementId: string, offset: number = 0): void {
+ const element = document.getElementById(elementId);
+ if (!element) {
+ return;
+ }
+
+ const elementPosition = element.getBoundingClientRect().top + window.scrollY;
+ const offsetPosition = elementPosition - offset;
+
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth',
+ });
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..4c583de
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,44 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ './index.html',
+ './src/**/*.{js,ts,jsx,tsx}',
+ ],
+ theme: {
+ extend: {
+ colors: {
+ cream: {
+ DEFAULT: '#FFFDF7',
+ light: '#FFFDF7',
+ dark: '#FFF8F0',
+ },
+ slate: {
+ 700: '#334155',
+ 800: '#1E293B',
+ 900: '#0F172A',
+ 500: '#64748B',
+ 400: '#94A3B8',
+ },
+ gold: {
+ light: '#E8D5A3',
+ DEFAULT: '#C8A951',
+ dark: '#B8963E',
+ medium: '#D4B968',
+ },
+ },
+ fontFamily: {
+ playfair: ['Playfair Display', 'Georgia', 'serif'],
+ inter: ['Inter', 'system-ui', 'sans-serif'],
+ },
+ maxWidth: {
+ '8xl': '88rem',
+ },
+ spacing: {
+ '18': '4.5rem',
+ '88': '22rem',
+ '128': '32rem',
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 0000000..945ec94
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,5 @@
+/**
+ * Test setup file for Vitest.
+ * Configures jsdom environment and extends matchers with jest-dom.
+ */
+import '@testing-library/jest-dom';
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..98e17d4
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,267 @@
+"""Validation tests for frontend configuration files.
+
+These tests verify that the project configuration files are correctly
+structured and contain the expected design tokens and settings.
+"""
+
+import json
+import os
+from pathlib import Path
+
+import pytest
+
+ROOT_DIR = Path(__file__).resolve().parent.parent
+
+
+def _read_file(relative_path: str) -> str:
+ """Read a file relative to the project root and return its content."""
+ file_path = ROOT_DIR / relative_path
+ assert file_path.exists(), f"File not found: {relative_path}"
+ return file_path.read_text(encoding="utf-8")
+
+
+class TestPackageJson:
+ """Tests for package.json structure and dependencies."""
+
+ @pytest.fixture()
+ def pkg(self) -> dict:
+ """Parse and return package.json as a dictionary."""
+ content = _read_file("package.json")
+ return json.loads(content)
+
+ def test_project_name(self, pkg: dict) -> None:
+ """Project should be named 'maddie-realestate'."""
+ assert pkg["name"] == "maddie-realestate"
+
+ def test_has_react_dependency(self, pkg: dict) -> None:
+ """React should be listed in dependencies."""
+ assert "react" in pkg["dependencies"]
+ assert "react-dom" in pkg["dependencies"]
+
+ def test_has_dev_dependencies(self, pkg: dict) -> None:
+ """Key dev dependencies should be present."""
+ dev_deps = pkg["devDependencies"]
+ assert "vite" in dev_deps
+ assert "tailwindcss" in dev_deps
+ assert "postcss" in dev_deps
+ assert "autoprefixer" in dev_deps
+ assert "@vitejs/plugin-react" in dev_deps
+ assert "typescript" in dev_deps
+
+ def test_has_scripts(self, pkg: dict) -> None:
+ """Essential npm scripts should be defined."""
+ scripts = pkg["scripts"]
+ assert "dev" in scripts
+ assert "build" in scripts
+ assert "preview" in scripts
+
+ def test_private_flag(self, pkg: dict) -> None:
+ """Package should be marked as private."""
+ assert pkg["private"] is True
+
+
+class TestIndexHtml:
+ """Tests for index.html content and meta tags."""
+
+ @pytest.fixture()
+ def html(self) -> str:
+ """Read and return index.html content."""
+ return _read_file("index.html")
+
+ def test_has_google_fonts_preconnect(self, html: str) -> None:
+ """Should have preconnect links for Google Fonts."""
+ assert "fonts.googleapis.com" in html
+ assert "fonts.gstatic.com" in html
+
+ def test_has_playfair_display_font(self, html: str) -> None:
+ """Should load Playfair Display from Google Fonts."""
+ assert "Playfair+Display" in html or "Playfair Display" in html
+
+ def test_has_inter_font(self, html: str) -> None:
+ """Should load Inter from Google Fonts."""
+ assert "Inter" in html
+
+ def test_has_smooth_scroll(self, html: str) -> None:
+ """Should have smooth scroll behavior on html element."""
+ assert "scroll-behavior" in html
+ assert "smooth" in html
+
+ def test_has_viewport_meta(self, html: str) -> None:
+ """Should have viewport meta tag."""
+ assert 'name="viewport"' in html
+
+ def test_has_description_meta(self, html: str) -> None:
+ """Should have a description meta tag."""
+ assert 'name="description"' in html
+
+ def test_has_root_div(self, html: str) -> None:
+ """Should have a root mount point div."""
+ assert 'id="root"' in html
+
+ def test_has_main_tsx_script(self, html: str) -> None:
+ """Should reference main.tsx as the entry module."""
+ assert "src/main.tsx" in html
+
+
+class TestTailwindConfig:
+ """Tests for tailwind.config.js design tokens."""
+
+ @pytest.fixture()
+ def config(self) -> str:
+ """Read and return tailwind.config.js content."""
+ return _read_file("tailwind.config.js")
+
+ def test_has_cream_color(self, config: str) -> None:
+ """Should define cream color token."""
+ assert "#FDF8F0" in config
+
+ def test_has_gold_color(self, config: str) -> None:
+ """Should define gold color token."""
+ assert "#C9A84C" in config
+
+ def test_has_warm_white_color(self, config: str) -> None:
+ """Should define warm-white color token."""
+ assert "#FAF7F2" in config
+
+ def test_has_slate_color(self, config: str) -> None:
+ """Should define slate color with value #334155."""
+ assert "#334155" in config
+
+ def test_has_playfair_font_family(self, config: str) -> None:
+ """Should configure Playfair Display font family."""
+ assert "playfair" in config
+ assert "Playfair Display" in config
+
+ def test_has_inter_font_family(self, config: str) -> None:
+ """Should configure Inter font family."""
+ assert "inter" in config.lower()
+ assert "Inter" in config
+
+ def test_has_content_paths(self, config: str) -> None:
+ """Should configure content paths for Tailwind purge."""
+ assert "./index.html" in config
+ assert "./src/**/*.{js,ts,jsx,tsx}" in config
+
+ def test_has_animations(self, config: str) -> None:
+ """Should define custom animations."""
+ assert "fade-in" in config
+ assert "slide-up" in config
+
+ def test_has_custom_spacing(self, config: str) -> None:
+ """Should extend spacing with custom values."""
+ assert "4.5rem" in config
+
+
+class TestPostcssConfig:
+ """Tests for postcss.config.js plugins."""
+
+ @pytest.fixture()
+ def config(self) -> str:
+ """Read and return postcss.config.js content."""
+ return _read_file("postcss.config.js")
+
+ def test_has_tailwindcss_plugin(self, config: str) -> None:
+ """Should include tailwindcss plugin."""
+ assert "tailwindcss" in config
+
+ def test_has_autoprefixer_plugin(self, config: str) -> None:
+ """Should include autoprefixer plugin."""
+ assert "autoprefixer" in config
+
+
+class TestViteConfig:
+ """Tests for vite.config.js settings."""
+
+ @pytest.fixture()
+ def config(self) -> str:
+ """Read and return vite.config.js content."""
+ return _read_file("vite.config.js")
+
+ def test_has_react_plugin(self, config: str) -> None:
+ """Should use the React Vite plugin."""
+ assert "@vitejs/plugin-react" in config
+ assert "react()" in config
+
+ def test_has_path_alias(self, config: str) -> None:
+ """Should define '@' path alias pointing to './src'."""
+ assert "'@'" in config or '"@"' in config
+ assert "./src" in config
+
+ def test_has_port_3000(self, config: str) -> None:
+ """Should configure dev server on port 3000."""
+ assert "3000" in config
+
+
+class TestTypeScriptConfig:
+ """Tests for tsconfig.json settings."""
+
+ @pytest.fixture()
+ def tsconfig(self) -> dict:
+ """Parse and return tsconfig.json as a dictionary."""
+ content = _read_file("tsconfig.json")
+ return json.loads(content)
+
+ def test_has_react_jsx(self, tsconfig: dict) -> None:
+ """Should use react-jsx for JSX transform."""
+ assert tsconfig["compilerOptions"]["jsx"] == "react-jsx"
+
+ def test_has_strict_mode(self, tsconfig: dict) -> None:
+ """Should enable strict mode."""
+ assert tsconfig["compilerOptions"]["strict"] is True
+
+ def test_has_path_alias(self, tsconfig: dict) -> None:
+ """Should define '@/*' path alias."""
+ paths = tsconfig["compilerOptions"]["paths"]
+ assert "@/*" in paths
+ assert "./src/*" in paths["@/*"]
+
+ def test_includes_src(self, tsconfig: dict) -> None:
+ """Should include the src directory."""
+ assert "src" in tsconfig["include"]
+
+
+class TestSourceFiles:
+ """Tests to verify essential source files exist and have correct content."""
+
+ def test_index_css_has_tailwind_directives(self) -> None:
+ """src/index.css should include Tailwind directives."""
+ css = _read_file("src/index.css")
+ assert "@tailwind base" in css
+ assert "@tailwind components" in css
+ assert "@tailwind utilities" in css
+
+ def test_index_css_has_base_typography(self) -> None:
+ """src/index.css should set base font families."""
+ css = _read_file("src/index.css")
+ assert "font-inter" in css
+ assert "font-playfair" in css
+
+ def test_main_tsx_exists(self) -> None:
+ """src/main.tsx should exist."""
+ assert (ROOT_DIR / "src" / "main.tsx").exists()
+
+ def test_app_tsx_exists(self) -> None:
+ """src/App.tsx should exist."""
+ assert (ROOT_DIR / "src" / "App.tsx").exists()
+
+ def test_vite_env_dts_exists(self) -> None:
+ """src/vite-env.d.ts should exist."""
+ assert (ROOT_DIR / "src" / "vite-env.d.ts").exists()
+
+ def test_types_index_exists(self) -> None:
+ """src/types/index.ts should exist."""
+ assert (ROOT_DIR / "src" / "types" / "index.ts").exists()
+
+ def test_scroll_utility_exists(self) -> None:
+ """src/utils/scrollTo.ts should exist."""
+ assert (ROOT_DIR / "src" / "utils" / "scrollTo.ts").exists()
+
+ def test_smooth_scroll_hook_exists(self) -> None:
+ """src/hooks/useSmoothScroll.ts should exist."""
+ assert (ROOT_DIR / "src" / "hooks" / "useSmoothScroll.ts").exists()
+
+ def test_app_has_all_sections(self) -> None:
+ """App.tsx should contain all major section IDs."""
+ app = _read_file("src/App.tsx")
+ for section_id in ["hero", "about", "sales", "contact"]:
+ assert f'id="{section_id}"' in app, f"Missing section: {section_id}"
diff --git a/tests/test_index_css.test.ts b/tests/test_index_css.test.ts
new file mode 100644
index 0000000..53826ca
--- /dev/null
+++ b/tests/test_index_css.test.ts
@@ -0,0 +1,87 @@
+/**
+ * Tests for src/index.css content.
+ * Validates that the global stylesheet contains required Tailwind directives,
+ * base styles, component classes, and custom utilities.
+ */
+import { describe, it, expect, beforeAll } from 'vitest';
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+
+let cssContent: string;
+
+beforeAll(() => {
+ cssContent = readFileSync(
+ resolve(__dirname, '..', 'src', 'index.css'),
+ 'utf-8',
+ );
+});
+
+describe('src/index.css', () => {
+ it('should include @tailwind base directive', () => {
+ expect(cssContent).toContain('@tailwind base;');
+ });
+
+ it('should include @tailwind components directive', () => {
+ expect(cssContent).toContain('@tailwind components;');
+ });
+
+ it('should include @tailwind utilities directive', () => {
+ expect(cssContent).toContain('@tailwind utilities;');
+ });
+
+ it('should set smooth scroll on html', () => {
+ expect(cssContent).toContain('scroll-behavior: smooth');
+ });
+
+ it('should set body font to Inter via font-inter', () => {
+ expect(cssContent).toContain('font-inter');
+ });
+
+ it('should set heading font to Playfair Display via font-playfair', () => {
+ expect(cssContent).toContain('font-playfair');
+ });
+
+ it('should define section-container component class', () => {
+ expect(cssContent).toContain('.section-container');
+ });
+
+ it('should define section-padding component class', () => {
+ expect(cssContent).toContain('.section-padding');
+ });
+
+ it('should define btn-primary component class', () => {
+ expect(cssContent).toContain('.btn-primary');
+ });
+
+ it('should define btn-secondary component class', () => {
+ expect(cssContent).toContain('.btn-secondary');
+ });
+
+ it('should define gold-gradient component class', () => {
+ expect(cssContent).toContain('.gold-gradient');
+ });
+
+ it('should define gold-gradient-text component class', () => {
+ expect(cssContent).toContain('.gold-gradient-text');
+ });
+
+ it('should define hover-lift animation class', () => {
+ expect(cssContent).toContain('.hover-lift');
+ });
+
+ it('should define hover-scale animation class', () => {
+ expect(cssContent).toContain('.hover-scale');
+ });
+
+ it('should define hover-glow animation class', () => {
+ expect(cssContent).toContain('.hover-glow');
+ });
+
+ it('should include webkit font smoothing', () => {
+ expect(cssContent).toContain('-webkit-font-smoothing: antialiased');
+ });
+
+ it('should set cream background on body', () => {
+ expect(cssContent).toContain('bg-cream');
+ });
+});
diff --git a/tests/test_index_html.test.ts b/tests/test_index_html.test.ts
new file mode 100644
index 0000000..af448c4
--- /dev/null
+++ b/tests/test_index_html.test.ts
@@ -0,0 +1,60 @@
+/**
+ * Tests for index.html structure and SEO attributes.
+ * Validates that the HTML entry point contains required meta tags,
+ * Google Fonts links, and the root mount point.
+ */
+import { describe, it, expect, beforeAll } from 'vitest';
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+
+let htmlContent: string;
+
+beforeAll(() => {
+ htmlContent = readFileSync(resolve(__dirname, '..', 'index.html'), 'utf-8');
+});
+
+describe('index.html', () => {
+ it('should have the correct title', () => {
+ expect(htmlContent).toContain('
Maddie | Luxury Real Estate');
+ });
+
+ it('should have a meta description', () => {
+ expect(htmlContent).toContain('name="description"');
+ expect(htmlContent).toContain('Luxury real estate');
+ });
+
+ it('should have a viewport meta tag', () => {
+ expect(htmlContent).toContain('name="viewport"');
+ expect(htmlContent).toContain('width=device-width');
+ });
+
+ it('should preconnect to Google Fonts', () => {
+ expect(htmlContent).toContain('href="https://fonts.googleapis.com"');
+ expect(htmlContent).toContain('href="https://fonts.gstatic.com"');
+ });
+
+ it('should load Playfair Display font', () => {
+ expect(htmlContent).toContain('Playfair+Display');
+ });
+
+ it('should load Inter font', () => {
+ expect(htmlContent).toContain('Inter');
+ });
+
+ it('should have smooth scroll on html element', () => {
+ expect(htmlContent).toContain('scroll-behavior: smooth');
+ });
+
+ it('should have a root div mount point', () => {
+ expect(htmlContent).toContain('
');
+ });
+
+ it('should reference main.tsx as module script', () => {
+ expect(htmlContent).toContain('src="/src/main.tsx"');
+ expect(htmlContent).toContain('type="module"');
+ });
+
+ it('should have lang="en" attribute', () => {
+ expect(htmlContent).toContain('lang="en"');
+ });
+});
diff --git a/tests/test_main.test.tsx b/tests/test_main.test.tsx
new file mode 100644
index 0000000..aa42f27
--- /dev/null
+++ b/tests/test_main.test.tsx
@@ -0,0 +1,28 @@
+/**
+ * Tests for the main entry point and App component rendering.
+ * Verifies that the App component mounts correctly into the DOM.
+ */
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import App from '../src/App';
+
+describe('App component', () => {
+ it('should render without crashing', () => {
+ const { container } = render(
);
+ expect(container).toBeTruthy();
+ });
+
+ it('should render the heading', () => {
+ render(
);
+ const heading = screen.getByRole('heading', { level: 1 });
+ expect(heading).toBeInTheDocument();
+ expect(heading).toHaveTextContent('Maddie | Luxury Real Estate');
+ });
+
+ it('should have a main element', () => {
+ render(
);
+ const main = screen.getByRole('main');
+ expect(main).toBeInTheDocument();
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..59f5b5d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "tests"]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..b4a98d2
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.js"]
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..e270b4e
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,21 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 3000,
+ open: true,
+ },
+ build: {
+ outDir: 'dist',
+ sourcemap: false,
+ },
+});
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..7abfbd4
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+/**
+ * Vite configuration for the Maddie Luxury Real Estate landing page.
+ * Configures React plugin, path aliases, and dev server.
+ */
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 3000,
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..7c9930c
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+
+/**
+ * Vitest configuration.
+ * Sets up jsdom environment and test file patterns.
+ */
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./tests/setup.ts'],
+ include: ['tests/**/*.test.{ts,tsx}'],
+ globals: true,
+ },
+});