Minastix Logo
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

  1. Follow the Technical Specs document to set up your project
  2. Implement missing components from this guide
  3. Copy all page and component files from Base44
  4. Test locally with npm run dev
  5. Configure your server for production deployment
  6. Set up CI/CD for automated deployments

© 2026 Minastix. All Rights Reserved.

Empowering children through movement and play.

Website designed and powered by Webtron