Module Architecture
CAFH Platform is organized into modules, each handling a specific domain:
- CMS Module - Content management
- CRM Module - Contact and campaign management
- Events Module - Calendar and activities
- Meetings Module - Zoom integration and feedback
- Analytics Module - Metrics and reporting
- Journey Module - Profile wizard and personalization
Each module consists of:
- Types - Data structures in
types.ts
- Storage - CRUD operations in
storage.ts
- Components - UI in
components/
- Routes - Navigation in
App.tsx
Creating a New Module
Let’s build a Donations Module from scratch.
Step 1: Define Types
export interface Donation {
id: string;
userId: string;
userName: string;
amount: number;
currency: string;
paymentMethod: 'Card' | 'PayPal' | 'Transfer';
status: 'Pending' | 'Completed' | 'Failed';
transactionId?: string;
createdAt: string;
completedAt?: string;
}
export interface DonationCampaign {
id: string;
title: string;
description: string;
goalAmount: number;
currentAmount: number;
currency: string;
startDate: string;
endDate: string;
isActive: boolean;
imageUrl?: string;
}
export interface DonationSettings {
paypalClientId?: string;
stripePublicKey?: string;
bankAccount?: string;
defaultCurrency: string;
thankYouMessage: string;
}
Step 2: Add Storage
const KEYS = {
// Existing keys...
DONATIONS: 'cafh_donations_v1',
DONATION_CAMPAIGNS: 'cafh_donation_campaigns_v1',
DONATION_SETTINGS: 'cafh_donation_settings_v1',
};
let donations: Donation[] = [];
let donationCampaigns: DonationCampaign[] = [];
let donationSettings: DonationSettings | null = null;
export const db = {
// Existing modules...
donations: {
// Donations CRUD
getAll: (): Donation[] => donations,
getByUser: (userId: string): Donation[] =>
donations.filter(d => d.userId === userId),
getByCampaign: (campaignId: string): Donation[] =>
donations.filter(d => d.campaignId === campaignId),
create: (donation: Donation): void => {
donations.push(donation);
localStorage.setItem(KEYS.DONATIONS, JSON.stringify(donations));
},
updateStatus: (id: string, status: Donation['status'], transactionId?: string): void => {
const donation = donations.find(d => d.id === id);
if (donation) {
donation.status = status;
donation.transactionId = transactionId;
if (status === 'Completed') {
donation.completedAt = new Date().toISOString();
}
localStorage.setItem(KEYS.DONATIONS, JSON.stringify(donations));
}
},
// Campaigns CRUD
campaigns: {
getAll: (): DonationCampaign[] => donationCampaigns,
getActive: (): DonationCampaign[] =>
donationCampaigns.filter(c => c.isActive),
create: (campaign: DonationCampaign): void => {
donationCampaigns.push(campaign);
localStorage.setItem(KEYS.DONATION_CAMPAIGNS, JSON.stringify(donationCampaigns));
},
update: (id: string, updates: Partial<DonationCampaign>): void => {
const index = donationCampaigns.findIndex(c => c.id === id);
if (index !== -1) {
donationCampaigns[index] = { ...donationCampaigns[index], ...updates };
localStorage.setItem(KEYS.DONATION_CAMPAIGNS, JSON.stringify(donationCampaigns));
}
},
},
// Settings
settings: {
get: (): DonationSettings | null => donationSettings,
update: (settings: DonationSettings): void => {
donationSettings = settings;
localStorage.setItem(KEYS.DONATION_SETTINGS, JSON.stringify(settings));
},
},
},
init: () => {
// Existing initializations...
donations = initStorage(KEYS.DONATIONS, []);
donationCampaigns = initStorage(KEYS.DONATION_CAMPAIGNS, []);
donationSettings = initStorage(KEYS.DONATION_SETTINGS, {
defaultCurrency: 'USD',
thankYouMessage: 'Thank you for your generous donation!',
});
},
};
Step 3: Create Admin Component
components/DonationsModule.tsx
import React, { useState, useEffect } from 'react';
import { Donation, DonationCampaign } from '../types';
import { db } from '../storage';
import { DollarSign, TrendingUp, Users } from 'lucide-react';
export const DonationsAdminView: React.FC = () => {
const [donations, setDonations] = useState<Donation[]>([]);
const [campaigns, setCampaigns] = useState<DonationCampaign[]>([]);
const [activeTab, setActiveTab] = useState<'donations' | 'campaigns'>('donations');
useEffect(() => {
setDonations(db.donations.getAll());
setCampaigns(db.donations.campaigns.getAll());
}, []);
const totalRaised = donations
.filter(d => d.status === 'Completed')
.reduce((sum, d) => sum + d.amount, 0);
return (
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">Donations</h1>
</div>
{/* KPIs */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Total Raised</p>
<p className="text-2xl font-bold">${totalRaised.toLocaleString()}</p>
</div>
<DollarSign className="w-10 h-10 text-green-600" />
</div>
</div>
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Total Donations</p>
<p className="text-2xl font-bold">{donations.length}</p>
</div>
<Users className="w-10 h-10 text-blue-600" />
</div>
</div>
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500">Active Campaigns</p>
<p className="text-2xl font-bold">{campaigns.filter(c => c.isActive).length}</p>
</div>
<TrendingUp className="w-10 h-10 text-purple-600" />
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-xl shadow">
<div className="border-b">
<button
className={`px-6 py-3 ${activeTab === 'donations' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('donations')}
>
Donations
</button>
<button
className={`px-6 py-3 ${activeTab === 'campaigns' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600'}`}
onClick={() => setActiveTab('campaigns')}
>
Campaigns
</button>
</div>
<div className="p-6">
{activeTab === 'donations' ? (
<DonationsTable donations={donations} />
) : (
<CampaignsTable campaigns={campaigns} />
)}
</div>
</div>
</div>
);
};
const DonationsTable: React.FC<{ donations: Donation[] }> = ({ donations }) => {
return (
<table className="w-full">
<thead>
<tr className="text-left border-b">
<th className="pb-2">Donor</th>
<th className="pb-2">Amount</th>
<th className="pb-2">Method</th>
<th className="pb-2">Status</th>
<th className="pb-2">Date</th>
</tr>
</thead>
<tbody>
{donations.map(d => (
<tr key={d.id} className="border-b">
<td className="py-3">{d.userName}</td>
<td>${d.amount}</td>
<td>{d.paymentMethod}</td>
<td>
<span className={`px-2 py-1 rounded text-xs ${
d.status === 'Completed' ? 'bg-green-100 text-green-800' :
d.status === 'Pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-red-100 text-red-800'
}`}>
{d.status}
</span>
</td>
<td>{new Date(d.createdAt).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
);
};
Step 4: Add Routes
import { DonationsAdminView } from './components/DonationsModule';
const App: React.FC = () => {
return (
<HashRouter>
<Routes>
{/* Existing routes... */}
<Route
path="/admin/donations"
element={
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.SUPER_ADMIN]}>
<AdminLayout>
<DonationsAdminView />
</AdminLayout>
</ProtectedRoute>
}
/>
</Routes>
</HashRouter>
);
};
import { DollarSign } from 'lucide-react';
const AdminLayout: React.FC = () => {
return (
<div>
<aside>
<nav>
{/* Existing menu items... */}
<Link to="/admin/donations" className="flex items-center gap-3 px-4 py-3">
<DollarSign className="w-5 h-5" />
<span>Donations</span>
</Link>
</nav>
</aside>
</div>
);
};
Module Best Practices
Modules should have minimal dependencies on each other. Use well-defined interfaces at module boundaries.
Follow naming conventions
- Types:
DonationCampaign, DonationSettings
- Storage keys:
cafh_donations_v1
- Components:
DonationsAdminView, DonationCard
- Routes:
/admin/donations
Track key metrics for your module:db.donations.getMetrics = () => ({
totalRaised: donations.reduce((sum, d) => sum + d.amount, 0),
averageDonation: donations.length ? totalRaised / donations.length : 0,
completionRate: donations.filter(d => d.status === 'Completed').length / donations.length,
});
- Test CRUD operations
- Verify data persistence
- Check edge cases (empty state, errors)
- Test with different user roles
Extending Existing Modules
To add features to existing modules:
- Add types - Extend interfaces in
types.ts
- Add methods - Add to
db.module in storage.ts
- Update UI - Modify existing components
- Add routes - If needed, add new routes in
App.tsx
Example: Add export to CRM:
export const db = {
crm: {
// Existing methods...
exportToCSV: (): string => {
const contacts = db.crm.contacts.getAll();
const csv = [
'Name,Email,Phone,Status',
...contacts.map(c => `${c.name},${c.email},${c.phone},${c.status}`)
].join('\n');
return csv;
},
},
};
Next Steps
Extending Types
Learn about TypeScript types
Storage System
Understand data persistence
Admin Guide
See how modules work from admin perspective