A modern, responsive laundry pickup booking microsite built with React, Vite, and TailwindCSS. Features dark/light theme toggle, professional pricing section, and a clean, accessible booking interface.
- Welcome Modal: Full-page popup on first visit with click-to-close functionality
- SweetAlert2 Modals: Beautiful, animated success/error modals for form feedback
- API Integration: Axios-based HTTP client with interceptors and error handling
- Service Architecture: Reusable API services to avoid code duplication
- Lazy Loading: Shimmer effects for pricing section while data loads
- Responsive Design: Mobile-first approach with TailwindCSS
- Dark/Light Theme: Toggle between themes with smooth transitions
- Professional Pricing: Three-tier pricing cards with popular plan highlighting
- Advanced Form Validation: React Hook Form + Yup with real-time validation
- Accessibility: WCAG AA compliant with proper ARIA labels and focus management
- Modern UI: Clean design inspired by AdminLTE with card-based layout
- React 18 - UI framework
- Vite - Build tool and dev server with HMR
- TailwindCSS 3 - Utility-first CSS framework with custom configuration
- PostCSS - CSS processing and optimization
- Autoprefixer - Automatic vendor prefixing
- React Hook Form - Performant forms with easy validation
- Yup - JavaScript schema builder for validation
- SweetAlert2 - Beautiful, responsive, customizable modals
- Axios - HTTP client for API requests with interceptors
- Inter Font - Modern, readable typography via Google Fonts
- SWC - Fast JavaScript/TypeScript compiler
- Node.js (version 16 or higher)
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd laundry-pickup-microsite- Install dependencies:
npm install- Start the development server:
npm run dev- Open your browser and navigate to
http://localhost:5173
npm run dev- Start development servernpm run build- Build for productionnpm run preview- Preview production buildnpm run lint- Run ESLint
laundry-pickup-microsite/
├── index.html # Root HTML file
├── package.json # Dependencies and scripts
├── vite.config.js # Vite configuration
├── src/
│ ├── main.jsx # Application entry point
│ ├── App.jsx # Main app component
│ ├── styles.css # CSS variables for theming
│ ├── services/ # API service layer
│ │ ├── api.js # Base API configuration and utilities
│ │ ├── pricing.js # Pricing-related API functions
│ │ └── orders.js # Order-related API functions
│ └── components/
│ ├── Form.jsx # Enhanced booking form with API integration
│ ├── Pricing.jsx # Pricing section with lazy loading
│ ├── Shimmer.jsx # Loading shimmer component
│ └── WelcomeModal.jsx # Welcome popup component
└── README.md # This file
- Welcome Modal (First Visit):
- Clear localStorage or use incognito mode to see the popup
- Notice the full-page overlay covering entire screen
- Click anywhere on the overlay or the close button to dismiss
- Verify smooth scroll to top after closing
- Note: Modal only appears once per browser session (uses localStorage)
- Reset: Run
localStorage.removeItem('laundry-visited')in console to test again
- Theme Toggle & Dark Mode Compatibility:
- Click the sun/moon icon in the header to switch between light and dark modes
- Pricing Section: Verify all buttons have proper contrast in both themes
- Non-popular Buttons: Should be light gray in light mode, dark gray in dark mode
- Popular Badge: Should remain blue with white text in both themes
- Tips Section: Yellow background should adapt to dark theme
- Form: Check all form elements remain visible in dark mode
- Modal: Test welcome modal in both themes
- Pricing Section:
- Review the three pricing tiers (Cuci Kering, Cuci + Setrika, VIP Express)
- Notice the "Paling Populer" badge on the middle tier
- Click "Pilih Paket Ini" buttons to scroll to the booking form
- API Integration & Lazy Loading:
- Shimmer Loading: Notice the pricing section shows shimmer effect on first load
- API Caching: Refresh page to see cached data loads instantly
- Error Handling: Disconnect internet to see fallback to dummy data
- Enhanced Form with API:
- Plan Selection: Choose different pricing plans from dropdown
- Weight Input: Enter laundry weight (1-50 kg) with validation
- Real-time Validation: Try typing invalid data to see instant feedback
- Name Field: Test with numbers/symbols (should show error)
- WhatsApp Field: Test with invalid formats (e.g., "abc123", "0812345678")
- Address Field: Test with very short text (< 10 chars)
- Character Counter: Notice the character counter for address field
- Submit with Errors: Try submitting with validation errors (warning modal appears)
- Success Modal: Submit valid data to see detailed success modal with order info
- Error Modal: Test error scenarios to see error modal with retry option
- Auto-reset: Form automatically resets after successful submission
- Responsiveness: Resize browser window to test mobile layout
- Accessibility: Use Tab key to navigate through form elements
Modify the CSS variables in src/styles.css to customize the color scheme:
:root {
--bg-primary: #FFFFFF;
--fg-primary: #000000;
--accent: #007ACC;
/* ... other variables */
}Edit the form component in src/components/Form.jsx to add or modify fields as needed.
Customize the pricing plans in src/components/Pricing.jsx:
const pricingPlans = [
{
id: 'basic',
name: 'Your Plan Name',
price: 'Rp XX.XXX',
perUnit: '/kg',
description: 'Your description',
features: ['Feature 1', 'Feature 2'],
popular: false, // Set to true for popular plan
color: 'border-gray-200 hover:border-gray-300'
}
];Customize the welcome popup in src/components/WelcomeModal.jsx:
// Change the title and description
<h1 id="welcome-title" className="text-3xl font-bold text-[var(--fg-primary)] mb-2">
Your Custom Title
</h1>
// Modify the features grid
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="p-4 bg-[var(--input-bg)] rounded-lg">
<div className="text-2xl mb-2">🚚</div>
<h3 className="font-semibold text-[var(--fg-primary)] mb-1">Your Feature</h3>
<p className="text-sm text-[var(--fg-primary)] opacity-75">Description</p>
</div>
</div>Customize validation rules in src/components/Form.jsx:
// Modify the Yup validation schema
const validationSchema = yup.object({
name: yup
.string()
.required('Custom error message')
.min(2, 'Minimum characters required')
.matches(/^[a-zA-Z\s]+$/, 'Only letters and spaces allowed'),
whatsapp: yup
.string()
.required('Phone number is required')
.matches(/^(\+62|62|0)[8-9][0-9]{7,11}$/, 'Invalid Indonesian phone number'),
address: yup
.string()
.required('Address is required')
.min(10, 'Address too short')
.max(200, 'Address too long')
});Customize modal behavior in src/components/Form.jsx:
// Success modal customization
await Swal.fire({
icon: 'success',
title: 'Custom Success Title',
text: 'Custom success message',
confirmButtonText: 'Custom Button Text',
confirmButtonColor: '#007ACC',
timer: 5000, // Auto close after 5 seconds
timerProgressBar: true,
// Add custom animations
showClass: {
popup: 'animate__animated animate__fadeInDown'
},
hideClass: {
popup: 'animate__animated animate__fadeOutUp'
}
});
// Error modal customization
await Swal.fire({
icon: 'error',
title: 'Custom Error Title',
text: 'Custom error message',
confirmButtonText: 'Try Again',
confirmButtonColor: '#007ACC'
});The project uses a fully configured Tailwind CSS setup with custom utilities and components:
/* In tailwind.config.js */
colors: {
'laundry': {
primary: '#007ACC', // Main brand color
secondary: '#6B7280', // Secondary text
success: '#10B981', // Success states
warning: '#F59E0B', // Warning states
danger: '#EF4444', // Error states
info: '#3B82F6', // Info states
}
}/* In src/styles.css */
@layer components {
.btn-primary {
@apply bg-[var(--accent)] text-white px-4 py-2 rounded-md font-medium
hover:bg-blue-600 focus:ring-2 focus:ring-[var(--accent)] focus:ring-offset-2
transition-colors duration-200;
}
.card {
@apply bg-[var(--card-bg)] border border-[var(--border)] rounded-lg shadow-md p-6;
}
.input-field {
@apply w-full p-3 border border-[var(--border)] rounded-md bg-[var(--input-bg)]
text-[var(--fg-primary)] focus:ring-2 focus:ring-[var(--accent)] focus:outline-none
transition-colors duration-200;
}
}/* Custom keyframes in tailwind.config.js */
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'bounce-slow': 'bounce 2s infinite',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'shimmer': 'shimmer 1.5s infinite',
}// Use custom colors
<div className="bg-laundry-primary text-white">Primary Button</div>
// Use custom components
<input className="input-field" placeholder="Enter text" />
// Use custom animations
<div className="animate-fade-in">Fade in content</div>
// Use utility classes
<div className="glass-effect backdrop-blur-sm">Glass morphism effect</div>- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
MIT License - feel free to use this project for your own purposes.