Project Overview: Your Client's Requirements
FlowMaster Plumbing & Heating is a local business that needs a modern, mobile-first website. They currently lose 65% of potential customers because their old site doesn't work on phones. Your mission: build them a lightning-fast, SEO-optimized solution using React + WordPress + Tailwind CSS.
Business Goals
📱 Mobile Conversions
Increase emergency calls by 40%
📅 Online Bookings
Enable 24/7 appointment scheduling
⭐ Trust Building
Showcase reviews and certifications
🔍 Local SEO
Rank #1 for "plumber near me"
Exercise Requirements
Technical Requirements Checklist
Frontend (React + Tailwind)
Backend (WordPress)
Step 1: WordPress Setup
First, let's set up the WordPress backend with custom post types and fields for our plumbing business.
// functions.php - Register Custom Post Types
function flowmaster_register_post_types() {
// Services Post Type
register_post_type('services', [
'labels' => [
'name' => 'Services',
'singular_name' => 'Service',
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-hammer',
'supports' => ['title', 'editor', 'thumbnail', 'custom-fields'],
'has_archive' => true,
'rewrite' => ['slug' => 'services'],
]);
// Reviews Post Type
register_post_type('reviews', [
'labels' => [
'name' => 'Reviews',
'singular_name' => 'Review',
],
'public' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-star-filled',
'supports' => ['title', 'editor', 'custom-fields'],
]);
}
add_action('init', 'flowmaster_register_post_types');
Adding REST API Fields
// Add REST API fields for custom data
function flowmaster_add_rest_fields() {
register_rest_field('services', 'service_details', [
'get_callback' => function($post) {
return [
'price_range' => get_field('price_range', $post['id']),
'duration' => get_field('duration', $post['id']),
'emergency' => get_field('emergency_available', $post['id']),
];
},
'schema' => null,
]);
}
add_action('rest_api_init', 'flowmaster_add_rest_fields');
Step 2: React Component Structure
Build mobile-first React components using Tailwind CSS for styling.
Main App Component
// App.js - Main Application Component
import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
const [services, setServices] = useState([]);
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const [servicesData, reviewsData] = await Promise.all([
WordPressAPI.getServices(),
WordPressAPI.getReviews()
]);
setServices(servicesData);
setReviews(reviewsData);
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-white">
<EmergencyBar />
<MobileNav />
<main>
<Hero />
<Services services={services} loading={loading} />
<Reviews reviews={reviews} />
<Contact />
</main>
<Footer />
</div>
);
}
export default App;
Emergency Bar Component
// components/EmergencyBar.js - Sticky Emergency CTA
import React from 'react';
const EmergencyBar = () => {
return (
<div className="sticky top-0 z-50 bg-red-600 text-white">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between py-2">
<span className="text-sm font-semibold">
24/7 Emergency Service
</span>
<a
href="tel:1-800-PLUMBER"
className="bg-white text-red-600 px-4 py-1 rounded-full"
>
Call Now
</a>
</div>
</div>
</div>
);
};
export default EmergencyBar;
Step 3: Mobile-First Service Cards
// components/Services.js - Responsive Service Grid
import React from 'react';
const Services = ({ services, loading }) => {
if (loading) {
return (
<div className="py-12 px-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map(i => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 rounded-lg h-48"></div>
</div>
))}
</div>
</div>
);
}
return (
<section className="py-12 px-4 bg-gray-50">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-8">
Book Your Service
</h2>
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto">
{/* Urgency Selector */}
<div className="grid grid-cols-2 gap-2 mb-4">
<button
type="button"
className={`py-3 px-4 rounded-lg ${
formData.urgency === 'emergency'
? 'bg-red-600 text-white'
: 'bg-gray-100'
}`}
>
🚨 Emergency
</button>
<button
type="button"
className={`py-3 px-4 rounded-lg ${
formData.urgency === 'regular'
? 'bg-blue-600 text-white'
: 'bg-gray-100'
}`}
>
📅 Schedule
</button>
</div>
{/* Form fields */}
<input
type="text"
placeholder="Your Name"
required
className="w-full px-4 py-3 rounded-lg border mb-4"
/>
<input
type="tel"
placeholder="Phone Number"
required
className="w-full px-4 py-3 rounded-lg border mb-4"
/>
<button
type="submit"
className="w-full bg-blue-600 text-white py-3 rounded-lg"
>
Book Appointment
</button>
</form>
</div>
</section>
);
};
export default Contact;
Step 6: API Service Layer
// services/api.js - WordPress API Integration
class WordPressAPIService {
constructor() {
this.baseURL = process.env.REACT_APP_WP_API_URL ||
'https://your-site.com/wp-json';
this.cache = new Map();
}
async fetchWithCache(endpoint, options = {}) {
const cacheKey = `${endpoint}`;
const cached = this.cache.get(cacheKey);
if (cached) {
return cached;
}
try {
const response = await fetch(
`${this.baseURL}${endpoint}`,
options
);
const data = await response.json();
this.cache.set(cacheKey, data);
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
async getServices() {
return this.fetchWithCache(
'/wp/v2/services?per_page=20'
);
}
async getReviews() {
return this.fetchWithCache(
'/wp/v2/reviews?per_page=10'
);
}
}
export const WordPressAPI = new WordPressAPIService();
Step 7: Tailwind CSS Configuration
// tailwind.config.js - Tailwind Configuration
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
emergency: {
500: '#ef4444',
600: '#dc2626',
}
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
}
},
},
plugins: [],
}
Package.json Dependencies
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"axios": "^1.3.0",
"tailwindcss": "^3.2.0",
"@heroicons/react": "^2.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
}
}
Step 8: Performance Optimization
// utils/performance.js - Performance Optimizations
import { useState, useEffect } from 'react';
// Lazy Loading Images Component
export const LazyImage = ({ src, alt, className, ...props }) => {
const [imageSrc, setImageSrc] = useState(null);
const [imageRef, setImageRef] = useState();
useEffect(() => {
let observer;
if (imageRef && imageSrc === null) {
if (IntersectionObserver) {
observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(imageRef);
}
});
},
{ threshold: 0.1, rootMargin: '50px' }
);
observer.observe(imageRef);
} else {
// Fallback for older browsers
setImageSrc(src);
}
}
return () => {
if (observer && observer.unobserve) {
observer.unobserve(imageRef);
}
};
}, [imageRef, imageSrc, src]);
return (
<img
ref={setImageRef}
src={imageSrc || '/placeholder.webp'}
alt={alt}
className={className}
loading="lazy"
{...props}
/>
);
};
// Debounce Function for Search
export const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
Step 9: Service Worker for Offline Support
// public/sw.js - Service Worker
const CACHE_NAME = 'flowmaster-v1';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/bundle.js',
'/offline.html'
];
// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
})
.catch(() => {
// Offline - return offline page
return caches.match('/offline.html');
})
);
});
Complete Solution Architecture
Testing Your Solution
Mobile Performance Checklist
SEO Checklist
Bonus: Advanced Features
SMS Integration
Add Twilio for text notifications:
Live Chat
Implement real-time support:
Price Calculator
Interactive pricing tool:
Customer Portal
Account management: