Admin Dashboard
CMS Technical

Mind Measure CMS - Technical Documentation

� Architecture Overview

The Mind Measure CMS is built as a React-based single-page application with a Supabase backend, designed for scalability and maintainability.

Technology Stack

Frontend:

  • React 18 with TypeScript
  • Vite for build tooling and development
  • Tailwind CSS for styling
  • React Router for client-side routing
  • Lucide React for icons

Backend:

  • Supabase (PostgreSQL database)
  • Row Level Security (RLS) for data access control
  • Supabase Storage for file uploads
  • Real-time subscriptions for live updates

Authentication:

  • Custom authentication system using Supabase Auth
  • University-scoped access control
  • Email domain validation

� Project Structure

src/
 components/
�    institutional/
�   �    UniversityScopedCMS.tsx          # Main CMS interface
�   �    cms/
�   �        UniversityOnboarding.tsx     # 7-step system
�   �        DemographicsManager.tsx      # Demographics with toggle
�   �        AcademicStructureManager.tsx # Faculties/schools
�   �        EmergencyServicesManager.tsx # Emergency resources
�   �        WellbeingContentLibrary.tsx  # Content management
�   �        ReportsAnalyticsConfig.tsx   # Reports system
�   �        AuthorizedUsersManager.tsx   # User management
�   �        ContentManager.tsx           # Article management
�   �        EmergencyResourcesManager.tsx # Emergency contacts
�    ui/                                  # Reusable UI components
�    UniversitySpecificLogin.tsx          # Authentication
 contexts/
�    AdminAuthContext.tsx                 # Authentication context
 services/
�    adminAuth.ts                         # Authentication service
 features/
�    cms/
�        data.ts                          # Data access layer
 Router.tsx                               # Application routing

� Database Schema

Core Tables

universities

CREATE TABLE universities (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  short_name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  website TEXT NOT NULL,
  contact_email TEXT NOT NULL,
  contact_phone TEXT,
  address TEXT,
  postcode TEXT,
  
  -- Student Demographics
  total_students INTEGER DEFAULT 0,
  undergraduate_students INTEGER DEFAULT 0,
  postgraduate_students INTEGER DEFAULT 0,
  international_students INTEGER DEFAULT 0,
  mature_students INTEGER DEFAULT 0,
  male_students INTEGER DEFAULT 0,
  female_students INTEGER DEFAULT 0,
  non_binary_students INTEGER DEFAULT 0,
  
  -- Branding
  primary_color TEXT DEFAULT '#0BA66D',
  secondary_color TEXT DEFAULT '#3b82f6',
  logo TEXT,
  logo_dark TEXT,
  campus_image TEXT,
  
  -- Authentication
  authorized_domains TEXT[] DEFAULT '{}',
  authorized_emails TEXT[] DEFAULT '{}',
  
  -- Emergency Resources
  emergency_contacts JSONB DEFAULT '[]',
  mental_health_services JSONB DEFAULT '[]',
  local_resources JSONB DEFAULT '[]',
  
  -- Metadata
  established INTEGER,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

university_authorized_users

CREATE TABLE university_authorized_users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  email TEXT NOT NULL,
  first_name TEXT,
  last_name TEXT,
  role TEXT DEFAULT 'admin',
  department TEXT,
  phone TEXT,
  status TEXT DEFAULT 'active',
  last_login TIMESTAMP WITH TIME ZONE,
  login_count INTEGER DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  
  UNIQUE(university_id, email)
);

Content Management Tables

-- Content Categories
CREATE TABLE content_categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  description TEXT,
  color TEXT DEFAULT '#3b82f6',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- Content Articles
CREATE TABLE content_articles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  category_id UUID REFERENCES content_categories(id) ON DELETE SET NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  excerpt TEXT,
  status TEXT DEFAULT 'draft',
  author_email TEXT,
  published_at TIMESTAMP WITH TIME ZONE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- Content Tags
CREATE TABLE content_tags (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  slug TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  
  UNIQUE(university_id, slug)
);
 
-- Article Tags (Many-to-Many)
CREATE TABLE article_tags (
  article_id UUID REFERENCES content_articles(id) ON DELETE CASCADE,
  tag_id UUID REFERENCES content_tags(id) ON DELETE CASCADE,
  PRIMARY KEY (article_id, tag_id)
);

Extended Schema Tables

-- Academic Structure
CREATE TABLE faculties (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  head_of_faculty_name TEXT,
  head_of_faculty_email TEXT,
  student_count INTEGER DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
CREATE TABLE schools (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  faculty_id UUID REFERENCES faculties(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  head_of_school_name TEXT,
  head_of_school_email TEXT,
  student_count INTEGER DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
CREATE TABLE departments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  school_id UUID REFERENCES schools(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  head_of_department_name TEXT,
  head_of_department_email TEXT,
  student_count INTEGER DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- Accommodation
CREATE TABLE halls_of_residence (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  capacity INTEGER,
  current_occupancy INTEGER DEFAULT 0,
  contact_name TEXT,
  contact_email TEXT,
  contact_phone TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- Wellbeing Content
CREATE TABLE wellbeing_tips (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  category_id UUID REFERENCES wellbeing_categories(id) ON DELETE SET NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  priority TEXT DEFAULT 'medium',
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
CREATE TABLE wellbeing_categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  color TEXT DEFAULT '#3b82f6',
  delivery_frequency TEXT DEFAULT 'weekly',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- Reports & Analytics
CREATE TABLE report_configurations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  description TEXT,
  type TEXT DEFAULT 'summary',
  frequency TEXT DEFAULT 'weekly',
  recipients TEXT[] DEFAULT '{}',
  selected_fields TEXT[] DEFAULT '{}',
  export_format TEXT DEFAULT 'pdf',
  is_active BOOLEAN DEFAULT true,
  last_generated TIMESTAMP WITH TIME ZONE,
  next_scheduled TIMESTAMP WITH TIME ZONE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
 
-- System Settings
CREATE TABLE settings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  university_id TEXT REFERENCES universities(id) ON DELETE CASCADE,
  key TEXT NOT NULL,
  value JSONB,
  description TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  
  UNIQUE(university_id, key)
);

Row Level Security (RLS)

University Data Access

-- Universities: Public read, authenticated write
CREATE POLICY "allow_public_select_universities" ON universities
  FOR SELECT USING (true);
 
CREATE POLICY "allow_authenticated_insert_universities" ON universities
  FOR INSERT WITH CHECK (auth.role() = 'authenticated');
 
CREATE POLICY "allow_university_update" ON universities
  FOR UPDATE USING (
    auth.email() LIKE '%@mindmeasure.co.uk' OR
    auth.email() = ANY(authorized_emails) OR
    SPLIT_PART(auth.email(), '@', 2) = ANY(authorized_domains)
  );

Content Access Control

-- Content Articles: University-scoped access
CREATE POLICY "university_content_access" ON content_articles
  FOR ALL USING (
    auth.email() LIKE '%@mindmeasure.co.uk' OR
    EXISTS (
      SELECT 1 FROM universities u 
      WHERE u.id = content_articles.university_id 
      AND (
        auth.email() = ANY(u.authorized_emails) OR
        SPLIT_PART(auth.email(), '@', 2) = ANY(u.authorized_domains)
      )
    )
  );

Helper Functions

-- Check if user is authorized for university
CREATE OR REPLACE FUNCTION is_user_authorized(university_id TEXT, user_email TEXT)
RETURNS BOOLEAN AS $$
BEGIN
  -- MM staff have access to all universities
  IF user_email LIKE '%@mindmeasure.co.uk' THEN
    RETURN TRUE;
  END IF;
  
  -- Check if user is in authorized emails or domain
  RETURN EXISTS (
    SELECT 1 FROM universities u 
    WHERE u.id = university_id 
    AND (
      user_email = ANY(u.authorized_emails) OR
      SPLIT_PART(user_email, '@', 2) = ANY(u.authorized_domains)
    )
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
 
-- Update user login tracking
CREATE OR REPLACE FUNCTION update_user_last_login(university_id TEXT, user_email TEXT)
RETURNS VOID AS $$
BEGIN
  INSERT INTO university_authorized_users (university_id, email, last_login, login_count)
  VALUES (university_id, user_email, NOW(), 1)
  ON CONFLICT (university_id, email) 
  DO UPDATE SET 
    last_login = NOW(),
    login_count = university_authorized_users.login_count + 1;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Component Architecture

Main CMS Interface (UniversityScopedCMS.tsx)

interface UniversityScopedCMSProps {
  universitySlug: string;
}
 
type CMSSection = 'dashboard' | 'profile' | 'emergency' | 'content' | 'users';
 
export function UniversityScopedCMS({ universitySlug }: UniversityScopedCMSProps) {
  const [activeSection, setActiveSection] = useState('dashboard');
  const [university, setUniversity] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Component renders different sections based on activeSection
  // Uses Tabs component for navigation
  // Implements university-scoped data loading
}

7-Step Onboarding System (UniversityOnboarding.tsx)

interface UniversityOnboardingProps {
  onBack: () => void;
  universityId?: string | null;
  onSave?: () => void;
}
 
export function UniversityOnboarding({ onBack, universityId, onSave }: UniversityOnboardingProps) {
  const [currentStep, setCurrentStep] = useState(1);
  const [formData, setFormData] = useState<UniversityFormData>({
    // All university fields with defaults
  });
  
  const totalSteps = 7;
  
  // Renders different components based on currentStep
  // Handles form data persistence
  // Provides navigation between steps
}

Numbers/Percentages Toggle System

interface ToggleableData {
  showAsPercentages: boolean;
  total: number;
  values: Record<string, number>;
}
 
const DemographicsManager: React.FC = () => {
  const [showAsPercentages, setShowAsPercentages] = useState(false);
  
  const calculatePercentages = (values: Record<string, number>, total: number) => {
    return Object.entries(values).reduce((acc, [key, value]) => {
      acc[key] = total > 0 ? Math.round((value / total) * 100) : 0;
      return acc;
    }, {} as Record<string, number>);
  };
  
  // Toggle between numbers and percentages view
  // Automatic validation and calculation
  // Visual indicators for data consistency
};

Data Access Layer (features/cms/data.ts)

University Operations

export interface University {
  id: string;
  name: string;
  short_name: string;
  slug: string;
  // ... all other fields
}
 
export async function getUniversityBySlug(slug: string): Promise<University | null> {
  const { data, error } = await supabase
    .from('universities')
    .select('*')
    .eq('slug', slug)
    .single();
    
  if (error) throw error;
  return data;
}
 
export async function updateUniversity(id: string, updates: Partial<University>): Promise<void> {
  const { error } = await supabase
    .from('universities')
    .update(updates)
    .eq('id', id);
    
  if (error) throw error;
}

Content Management

export interface ContentArticle {
  id: string;
  university_id: string;
  category_id?: string;
  title: string;
  content: string;
  status: 'draft' | 'review' | 'published' | 'archived';
  // ... other fields
}
 
export async function getContentArticles(universityId: string): Promise<ContentArticle[]> {
  const { data, error } = await supabase
    .from('content_articles')
    .select(`
      *,
      category:content_categories(name, color),
      tags:article_tags(tag:content_tags(name))
    `)
    .eq('university_id', universityId)
    .order('created_at', { ascending: false });
    
  if (error) throw error;
  return data || [];
}

User Management

export interface AuthorizedUser {
  id: string;
  university_id: string;
  email: string;
  first_name?: string;
  last_name?: string;
  role: string;
  status: 'active' | 'inactive' | 'pending';
  // ... other fields
}
 
export async function isUserAuthorized(universityId: string, email: string): Promise<boolean> {
  const { data } = await supabase.rpc('is_user_authorized', {
    university_id: universityId,
    user_email: email
  });
  
  return data || false;
}

Styling System

Tailwind Configuration

// tailwind.config.ts
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        'mm-green': '#0BA66D',
        'mm-blue': '#3b82f6',
        // Mind Measure brand colors
      }
    }
  }
}

Component Styling Patterns

// Consistent card styling
const cardClasses = "bg-white border border-slate-200 rounded-lg shadow-sm";
 
// Status-based styling
const getStatusColor = (status: string) => {
  switch (status) {
    case 'active': return 'bg-green-100 text-green-800';
    case 'draft': return 'bg-yellow-100 text-yellow-800';
    case 'published': return 'bg-blue-100 text-blue-800';
    default: return 'bg-gray-100 text-gray-800';
  }
};
 
// Responsive grid patterns
const gridClasses = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4";

State Management

Authentication Context

interface AdminAuthContextType {
  user: User | null;
  loading: boolean;
  signIn: (email: string, password: string) => Promise<void>;
  signOut: () => Promise<void>;
  isAuthenticated: boolean;
}
 
export const AdminAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  
  // Handles authentication state
  // Provides auth methods to components
  // Manages session persistence
};

Form State Management

// University onboarding form state
const [formData, setFormData] = useState<UniversityFormData>({
  // Default values for all fields
  name: '',
  total_students: 0,
  // ... etc
});
 
// Partial updates
const updateFormData = (updates: Partial<UniversityFormData>) => {
  setFormData(prev => ({ ...prev, ...updates }));
};
 
// Validation state
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});

Deployment

Environment Variables

# Production
VITE_SUPABASE_URL=https://ewrrictbejcmdkgpvkio.supabase.co
VITE_SUPABASE_ANON_KEY=your_production_anon_key
VITE_APP_URL=https://admin.mindmeasure.co.uk

# Development
VITE_SUPABASE_URL=https://ewrrictbejcmdkgpvkio.supabase.co
VITE_SUPABASE_ANON_KEY=your_development_anon_key
VITE_APP_URL=http://localhost:3000

Build Configuration

// vite.config.ts
export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          supabase: ['@supabase/supabase-js']
        }
      }
    }
  }
});

Vercel Deployment

// vercel.json
{
  "framework": "vite",
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self' https://api.mindmeasure.co.uk https://*.mindmeasure.co.uk;"
        }
      ]
    }
  ]
}

Testing

Component Testing

// Example test for DemographicsManager
import { render, screen, fireEvent } from '@testing-library/react';
import { DemographicsManager } from './DemographicsManager';
 
describe('DemographicsManager', () => {
  it('toggles between numbers and percentages', () => {
    render(<DemographicsManager formData={mockData} setFormData={mockSetFormData} />);
    
    const toggleButton = screen.getByRole('button', { name: /calculator/i });
    fireEvent.click(toggleButton);
    
    expect(screen.getByText(/show percentages/i)).toBeInTheDocument();
  });
});

Database Testing

-- Test data setup
INSERT INTO universities (id, name, short_name, slug, contact_email, website)
VALUES ('test-uni', 'Test University', 'TestU', 'test', 'admin@test.edu', 'https://test.edu');
 
-- Test RLS policies
SET ROLE authenticated;
SET request.jwt.claims TO '{"email": "admin@test.edu"}';
 
SELECT * FROM universities WHERE id = 'test-uni';
-- Should return the test university

Performance Considerations

Code Splitting

// Lazy load CMS components
const UniversityOnboarding = lazy(() => import('./cms/UniversityOnboarding'));
const EmergencyResourcesManager = lazy(() => import('./cms/EmergencyResourcesManager'));
 
// Wrap in Suspense
<Suspense fallback={<LoadingSpinner />}>
  <UniversityOnboarding />
</Suspense>

Data Optimization

// Selective field loading
const { data } = await supabase
  .from('universities')
  .select('id, name, short_name, slug') // Only needed fields
  .eq('slug', slug)
  .single();
 
// Pagination for large datasets
const { data } = await supabase
  .from('content_articles')
  .select('*')
  .range(start, end)
  .order('created_at', { ascending: false });

Caching Strategy

// React Query for data caching
import { useQuery } from '@tanstack/react-query';
 
const useUniversity = (slug: string) => {
  return useQuery({
    queryKey: ['university', slug],
    queryFn: () => getUniversityBySlug(slug),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
};

Monitoring & Debugging

Error Handling

// Global error boundary
class CMSErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('CMS Error:', error, errorInfo);
    // Send to monitoring service
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

Logging

// Structured logging
const logger = {
  info: (message: string, data?: any) => {
    console.log(`[CMS] ${message}`, data);
  },
  error: (message: string, error?: Error) => {
    console.error(`[CMS ERROR] ${message}`, error);
  }
};
 
// Usage
logger.info('University loaded', { universityId, slug });
logger.error('Failed to save university', error);

Development Workflow

Local Development

# Install dependencies
npm install
 
# Start development server
npm run dev
 
# Run tests
npm test
 
# Build for production
npm run build
 
# Preview production build
npm run preview

Database Migrations

# Create new migration
supabase migration new add_new_feature
 
# Apply migrations
supabase db push
 
# Reset database (development only)
supabase db reset

Code Quality

# Linting
npm run lint
 
# Type checking
npm run type-check
 
# Format code
npm run format

Additional Resources


This technical documentation provides comprehensive coverage of the Mind Measure CMS architecture, implementation details, and development practices. For user-facing documentation, refer to the CMS User Guide.