Implementation & Troubleshooting Guide
Missing Components & Implementations
This guide provides complete implementations for all missing components, utilities, and configurations needed for your self-hosted Minastix application.
1. Fix Home.jsx Motion Import
Issue
The motion component is used but not imported.
Solution
// At the top of Home.jsx, add:
import { motion } from 'framer-motion';
// Example usage:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{/* Your content */}
</motion.div>2. Missing Component: ProgramCard
src/components/home/ProgramCard.jsx
import React from 'react';
import { motion } from 'framer-motion';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
export default function ProgramCard({ program, onSelect }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3 }}
>
<Card className={`bg-slate-800 border-2 ${program.color} hover:shadow-2xl transition-all cursor-pointer h-full`}>
<CardHeader>
<div className="w-full h-48 rounded-lg overflow-hidden mb-4">
<img
src={program.image}
alt={program.name}
className="w-full h-full object-cover"
/>
</div>
<CardTitle className="text-white text-2xl">{program.name}</CardTitle>
<p className="text-slate-400 text-sm">{program.ageRange}</p>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-slate-300">{program.description}</p>
<Button
onClick={onSelect}
className="w-full bg-lime-500 hover:bg-lime-600 text-slate-900 font-bold"
>
Enroll Now <ArrowRight className="w-4 h-4 ml-2" />
</Button>
</CardContent>
</Card>
</motion.div>
);
}3. Missing Component: HeroSection
src/components/home/HeroSection.jsx
import React from 'react';
import { motion } from 'framer-motion';
import { Button } from '@/components/ui/button';
import { useNavigate } from 'react-router-dom';
import { createPageUrl } from '@/utils';
export default function HeroSection() {
const navigate = useNavigate();
return (
<section className="relative min-h-[80vh] flex items-center justify-center overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-b from-slate-900 via-slate-800 to-transparent" />
{/* Content */}
<div className="relative z-10 container mx-auto px-4 text-center">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<img
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/69010ca5cc964c7fd44065a4/7d0b9ad55_Minastix.png"
alt="Minastix Logo"
className="h-32 mx-auto mb-8"
/>
<h1 className="text-5xl md:text-7xl font-black text-white mb-6">
Empowering Children Through Movement
</h1>
<p className="text-xl md:text-2xl text-slate-300 mb-8 max-w-3xl mx-auto">
Structured, age-appropriate physical development programs for children aged 2 months to 12+ years
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
onClick={() => navigate(createPageUrl('Enroll'))}
className="bg-lime-500 hover:bg-lime-600 text-slate-900 font-bold text-lg px-8 py-6"
>
Enroll Your Child Now
</Button>
<Button
onClick={() => navigate(createPageUrl('Shop'))}
variant="outline"
className="border-white text-white hover:bg-white hover:text-slate-900 text-lg px-8 py-6"
>
Shop Uniforms
</Button>
</div>
</motion.div>
</div>
</section>
);
}4. Utility Functions Implementation
src/utils/index.js
/**
* Creates a URL for a page with optional query parameters
* @param {string} pageName - Name of the page
* @param {object} params - Optional query parameters
* @returns {string} - Complete URL path
*/
export function createPageUrl(pageName, params = {}) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
searchParams.append(key, value);
}
});
const queryString = searchParams.toString();
return `/${pageName}${queryString ? '?' + queryString : ''}`;
}
/**
* Format currency in South African Rand
* @param {number} amount - Amount to format
* @returns {string} - Formatted currency string
*/
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-ZA', {
style: 'currency',
currency: 'ZAR',
}).format(amount);
}
/**
* Calculate age from date of birth
* @param {string} dateOfBirth - Date of birth in YYYY-MM-DD format
* @returns {number} - Age in years
*/
export function calculateAge(dateOfBirth) {
const today = new Date();
const birthDate = new Date(dateOfBirth);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
/**
* Validate email address
* @param {string} email - Email to validate
* @returns {boolean} - Whether email is valid
*/
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validate South African phone number
* @param {string} phone - Phone number to validate
* @returns {boolean} - Whether phone is valid
*/
export function isValidPhoneNumber(phone) {
const phoneRegex = /^(\+27|0)[0-9]{9}$/;
return phoneRegex.test(phone.replace(/\s/g, ''));
}5. Error Boundary Implementation
src/components/ErrorBoundary.jsx
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { AlertTriangle, RefreshCw } from 'lucide-react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
this.setState({ error, errorInfo });
}
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center p-4">
<Card className="bg-slate-800 border-red-500/50 max-w-2xl w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-white">
<AlertTriangle className="w-6 h-6 text-red-400" />
Something Went Wrong
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-slate-300">
We're sorry, but something unexpected happened. Please try refreshing the page.
</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="bg-slate-900 p-4 rounded text-xs">
<summary className="text-red-400 cursor-pointer mb-2">
Error Details (Development Only)
</summary>
<pre className="text-red-300 whitespace-pre-wrap overflow-x-auto">
{this.state.error.toString()}
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
<Button
onClick={this.handleReset}
className="bg-lime-500 hover:bg-lime-600 text-slate-900 w-full"
>
<RefreshCw className="w-4 h-4 mr-2" />
Go to Home Page
</Button>
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;Usage in App.jsx
import ErrorBoundary from './components/ErrorBoundary';
export default function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppContent />
</BrowserRouter>
</QueryClientProvider>
</ErrorBoundary>
);
}6. Loading State Component
src/components/LoadingSpinner.jsx
import React from 'react';
import { motion } from 'framer-motion';
export default function LoadingSpinner({ text = 'Loading...', fullScreen = false }) {
const Container = fullScreen ? 'div' : 'div';
const containerClass = fullScreen
? 'fixed inset-0 flex items-center justify-center bg-slate-900/80 backdrop-blur-sm z-50'
: 'flex items-center justify-center py-8';
return (
<Container className={containerClass}>
<div className="text-center">
<motion.div
className="w-16 h-16 border-4 border-lime-400 border-t-transparent rounded-full mx-auto mb-4"
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
/>
<p className="text-white text-lg">{text}</p>
</div>
</Container>
);
}
// Skeleton loader for lists
export function SkeletonLoader({ count = 3 }) {
return (
<div className="space-y-4">
{[...Array(count)].map((_, i) => (
<div key={i} className="bg-slate-800 rounded-lg p-6 animate-pulse">
<div className="h-6 bg-slate-700 rounded w-3/4 mb-4" />
<div className="h-4 bg-slate-700 rounded w-full mb-2" />
<div className="h-4 bg-slate-700 rounded w-5/6" />
</div>
))}
</div>
);
}7. Form Validation Hook
src/hooks/useFormValidation.js
import { useState } from 'react';
import { isValidEmail, isValidPhoneNumber } from '@/utils';
export function useFormValidation(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (fieldName, value) => {
const rules = validationRules[fieldName];
if (!rules) return null;
if (rules.required && (!value || value.trim() === '')) {
return `${fieldName} is required`;
}
if (rules.email && value && !isValidEmail(value)) {
return 'Invalid email address';
}
if (rules.phone && value && !isValidPhoneNumber(value)) {
return 'Invalid phone number';
}
if (rules.minLength && value && value.length < rules.minLength) {
return `Must be at least ${rules.minLength} characters`;
}
if (rules.maxLength && value && value.length > rules.maxLength) {
return `Must be no more than ${rules.maxLength} characters`;
}
if (rules.pattern && value && !rules.pattern.test(value)) {
return rules.patternMessage || 'Invalid format';
}
if (rules.custom) {
return rules.custom(value, values);
}
return null;
};
const handleChange = (fieldName, value) => {
setValues(prev => ({ ...prev, [fieldName]: value }));
if (touched[fieldName]) {
const error = validate(fieldName, value);
setErrors(prev => ({ ...prev, [fieldName]: error }));
}
};
const handleBlur = (fieldName) => {
setTouched(prev => ({ ...prev, [fieldName]: true }));
const error = validate(fieldName, values[fieldName]);
setErrors(prev => ({ ...prev, [fieldName]: error }));
};
const validateAll = () => {
const newErrors = {};
let isValid = true;
Object.keys(validationRules).forEach(fieldName => {
const error = validate(fieldName, values[fieldName]);
if (error) {
newErrors[fieldName] = error;
isValid = false;
}
});
setErrors(newErrors);
setTouched(
Object.keys(validationRules).reduce((acc, key) => ({ ...acc, [key]: true }), {})
);
return isValid;
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateAll,
reset,
setValues,
};
}
// Example usage:
// const { values, errors, touched, handleChange, handleBlur, validateAll } = useFormValidation(
// { email: '', phone: '', name: '' },
// {
// email: { required: true, email: true },
// phone: { required: true, phone: true },
// name: { required: true, minLength: 2 }
// }
// );8. Toast Notifications Setup
Add to App.jsx
import { Toaster } from 'sonner';
export default function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppContent />
</BrowserRouter>
<Toaster
position="top-right"
toastOptions={{
style: {
background: '#1e293b',
color: '#fff',
border: '1px solid #334155',
},
}}
/>
</QueryClientProvider>
</ErrorBoundary>
);
}Usage Example
import { toast } from 'sonner';
// Success toast
toast.success('Enrollment added to cart!');
// Error toast
toast.error('Failed to process payment');
// Info toast
toast.info('Please fill in all required fields');
// Loading toast
const loadingToast = toast.loading('Processing payment...');
// Later:
toast.success('Payment successful!', { id: loadingToast });9. Accessibility Improvements
Accessible Button Component
import React from 'react';
import { Button as BaseButton } from '@/components/ui/button';
export function AccessibleButton({
children,
ariaLabel,
disabled,
loading,
...props
}) {
return (
<BaseButton
{...props}
aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
aria-disabled={disabled || loading}
disabled={disabled || loading}
role="button"
tabIndex={disabled ? -1 : 0}
>
{loading ? (
<>
<span className="sr-only">Loading...</span>
<span aria-hidden="true">{children}</span>
</>
) : (
children
)}
</BaseButton>
);
}Form Input with Labels
export function FormInput({
label,
error,
id,
required,
...props
}) {
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<div className="space-y-2">
<label
htmlFor={inputId}
className="block text-sm font-medium text-white"
>
{label}
{required && <span className="text-red-400 ml-1" aria-label="required">*</span>}
</label>
<input
id={inputId}
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-error` : undefined}
aria-required={required}
className={`w-full p-3 rounded-lg bg-slate-700 text-white border ${
error ? 'border-red-500' : 'border-slate-600'
} focus:ring-2 focus:ring-lime-400 focus:outline-none`}
{...props}
/>
{error && (
<p
id={`${inputId}-error`}
className="text-red-400 text-sm"
role="alert"
>
{error}
</p>
)}
</div>
);
}10. Environment Variables Configuration
.env.example
# Application Configuration VITE_BASE44_APP_ID=69010ca5cc964c7fd44065a4 VITE_BASE44_API_URL=https://base44.app/api # PayFast Configuration VITE_PAYFAST_MERCHANT_ID=14324979 VITE_PAYFAST_MERCHANT_KEY=67nxtyzug6iwe VITE_PAYFAST_PASSPHRASE=Passw0rd2022 VITE_PAYFAST_URL=https://www.payfast.co.za/eng/process # Environment VITE_ENVIRONMENT=production # Optional: Analytics VITE_GOOGLE_ANALYTICS_ID= VITE_SENTRY_DSN=
src/config/env.js
export const config = {
base44: {
appId: import.meta.env.VITE_BASE44_APP_ID,
apiUrl: import.meta.env.VITE_BASE44_API_URL,
},
payfast: {
merchantId: import.meta.env.VITE_PAYFAST_MERCHANT_ID,
merchantKey: import.meta.env.VITE_PAYFAST_MERCHANT_KEY,
passphrase: import.meta.env.VITE_PAYFAST_PASSPHRASE,
url: import.meta.env.VITE_PAYFAST_URL,
},
environment: import.meta.env.VITE_ENVIRONMENT || 'development',
isDevelopment: import.meta.env.DEV,
isProduction: import.meta.env.PROD,
};
// Validate required environment variables
const requiredVars = [
'VITE_BASE44_APP_ID',
'VITE_BASE44_API_URL',
];
const missingVars = requiredVars.filter(
(varName) => !import.meta.env[varName]
);
if (missingVars.length > 0) {
console.error(
'Missing required environment variables:',
missingVars.join(', ')
);
}11. API Error Handling
src/utils/apiErrorHandler.js
import { toast } from 'sonner';
export class APIError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'APIError';
this.status = status;
this.data = data;
}
}
export async function handleAPICall(apiFunction, options = {}) {
const {
showSuccessToast = false,
successMessage = 'Operation successful',
showErrorToast = true,
errorMessage = 'An error occurred',
onError,
} = options;
try {
const result = await apiFunction();
if (showSuccessToast) {
toast.success(successMessage);
}
return { success: true, data: result };
} catch (error) {
console.error('API Error:', error);
const apiError = new APIError(
error.message || errorMessage,
error.status || 500,
error.data
);
if (showErrorToast) {
toast.error(apiError.message);
}
if (onError) {
onError(apiError);
}
return { success: false, error: apiError };
}
}
// Usage example:
// const { success, data, error } = await handleAPICall(
// () => base44.entities.Enrollment.create(enrollmentData),
// {
// showSuccessToast: true,
// successMessage: 'Enrollment created!',
// errorMessage: 'Failed to create enrollment',
// }
// );12. React Query Configuration
Enhanced Query Client Setup
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 1,
refetchOnWindowFocus: false,
onError: (error) => {
console.error('Query Error:', error);
toast.error(error.message || 'Failed to fetch data');
},
},
mutations: {
onError: (error) => {
console.error('Mutation Error:', error);
toast.error(error.message || 'Operation failed');
},
},
},
});13. Testing Setup
Install Testing Dependencies
npm install -D @testing-library/react @testing-library/jest-dom \ @testing-library/user-event vitest jsdom
vite.config.js (add test config)
export default defineConfig({
// ... existing config
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
})src/test/setup.js
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom';
afterEach(() => {
cleanup();
});Example Test: src/components/__tests__/ProgramCard.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import ProgramCard from '../home/ProgramCard';
describe('ProgramCard', () => {
const mockProgram = {
id: 'test',
name: 'Test Program',
ageRange: '2-5 years',
description: 'Test description',
color: 'border-blue-400',
image: 'https://example.com/image.jpg',
};
it('renders program information', () => {
render(<ProgramCard program={mockProgram} onSelect={() => {}} />);
expect(screen.getByText('Test Program')).toBeInTheDocument();
expect(screen.getByText('2-5 years')).toBeInTheDocument();
expect(screen.getByText('Test description')).toBeInTheDocument();
});
it('calls onSelect when button is clicked', () => {
const handleSelect = vi.fn();
render(<ProgramCard program={mockProgram} onSelect={handleSelect} />);
fireEvent.click(screen.getByText(/Enroll Now/i));
expect(handleSelect).toHaveBeenCalledTimes(1);
});
});package.json (add test script)
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}14. CI/CD Pipeline (GitHub Actions)
.github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
env:
VITE_BASE44_APP_ID: ${{ secrets.VITE_BASE44_APP_ID }}
VITE_BASE44_API_URL: ${{ secrets.VITE_BASE44_API_URL }}
- name: Deploy to server
if: github.ref == 'refs/heads/main'
run: |
# Add your deployment commands here
# Example: rsync, scp, or use a deployment action
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SERVER_HOST: ${{ secrets.SERVER_HOST }}15. README.md Documentation
# Minastix - Self-Hosted Frontend ## Overview Self-hosted React frontend for Minastix enrollment and e-commerce platform, integrated with Base44 backend services. ## Prerequisites - Node.js 18+ - npm or yarn - Base44 account with API access ## Installation ```bash # Clone repository git clone <your-repo-url> cd minastix-frontend # Install dependencies npm install # Copy environment variables cp .env.example .env # Edit .env with your credentials ``` ## Development ```bash # Start development server npm run dev # Run tests npm test # Build for production npm run build ``` ## Environment Variables See `.env.example` for required variables: - `VITE_BASE44_APP_ID` - Your Base44 application ID - `VITE_BASE44_API_URL` - Base44 API endpoint ## Project Structure ``` src/ ├── api/ # Base44 client configuration ├── components/ # React components ├── pages/ # Page components ├── utils/ # Utility functions ├── hooks/ # Custom React hooks └── config/ # Configuration files ``` ## Deployment See `DEPLOYMENT.md` for detailed deployment instructions. ## Testing ```bash npm test # Run tests npm run test:ui # Run tests with UI npm run test:coverage # Generate coverage report ``` ## Support For issues or questions, contact: info@monkeynastix.com
16. Complete Checklist
Implementation Checklist
✅ Next Steps
- Follow the Technical Specs document to set up your project
- Implement missing components from this guide
- Copy all page and component files from Base44
- Test locally with npm run dev
- Configure your server for production deployment
- Set up CI/CD for automated deployments
