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:3000Build 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 universityPerformance 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 previewDatabase Migrations
# Create new migration
supabase migration new add_new_feature
# Apply migrations
supabase db push
# Reset database (development only)
supabase db resetCode Quality
# Linting
npm run lint
# Type checking
npm run type-check
# Format code
npm run formatAdditional Resources
- Supabase Documentation: https://supabase.com/docs (opens in a new tab)
- React Documentation: https://react.dev (opens in a new tab)
- Tailwind CSS: https://tailwindcss.com/docs (opens in a new tab)
- TypeScript Handbook: https://www.typescriptlang.org/docs (opens in a new tab)
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.