![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/ |
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
import { FiStar, FiLock, FiUnlock, FiAward, FiEye, FiEyeOff, FiClock, FiTrendingUp, FiUsers, FiBookOpen } from 'react-icons/fi';
interface SocietyDegreeTrackerProps {
userId: string;
showProgress?: boolean;
showCeremonies?: boolean;
}
interface UserDegree {
id: string;
achievedAt: string;
ceremonyCompleted: boolean;
ceremonyDate?: string;
progressPercentage: number;
isCurrentTarget: boolean;
degree: {
id: string;
degreeNumber: number;
name: string;
title?: string;
description: string;
xpRequired: number;
symbol?: string;
color?: string;
lodgeLevel: string;
isSecret: boolean;
privileges: string[];
requirements: {
xp: number;
cases: number;
clients: number;
proBono: number;
mentorship: number;
winRate?: number;
timeRequirement?: number;
};
};
}
interface NextDegree {
id: string;
degreeNumber: number;
name: string;
title?: string;
description: string;
xpRequired: number;
symbol?: string;
color?: string;
lodgeLevel: string;
isSecret: boolean;
requirements: {
xp: number;
cases: number;
clients: number;
proBono: number;
mentorship: number;
winRate?: number;
timeRequirement?: number;
};
progressPercentage: number;
}
interface UserStats {
currentXP: number;
totalCases: number;
totalClients: number;
proBonoHours: number;
mentorshipSessions: number;
winRate: number;
}
interface DegreeProgressData {
currentDegree?: UserDegree;
nextDegree?: NextDegree;
allDegrees: UserDegree[];
userStats: UserStats;
lodgeMemberships: Array<{
lodge: {
name: string;
lodgeLevel: string;
description: string;
};
role: string;
joinedDate: string;
}>;
}
const SocietyDegreeTracker: React.FC<SocietyDegreeTrackerProps> = ({
userId,
showProgress = true,
showCeremonies = true
}) => {
const [data, setData] = useState<DegreeProgressData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'overview' | 'degrees' | 'lodges' | 'ceremonies'>('overview');
useEffect(() => {
const fetchDegreeProgress = async () => {
try {
setLoading(true);
const response = await fetch(`/api/user/${userId}/society-progress`);
if (!response.ok) {
throw new Error('Failed to fetch degree progress');
}
const progressData = await response.json();
setData(progressData);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
if (userId) {
fetchDegreeProgress();
}
}, [userId]);
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
if (error) {
return (
<div className="text-center p-8">
<p className="text-red-600">Error loading degree progress: {error}</p>
</div>
);
}
if (!data) {
return (
<div className="text-center p-8">
<p className="text-gray-600">No degree data available</p>
</div>
);
}
const { currentDegree, nextDegree, allDegrees, userStats, lodgeMemberships } = data;
const getLodgeLevelColor = (level: string) => {
switch (level) {
case 'BLUE': return 'text-blue-600 bg-blue-100';
case 'RED': return 'text-red-600 bg-red-100';
case 'BLACK': return 'text-gray-800 bg-gray-200';
default: return 'text-gray-600 bg-gray-100';
}
};
const formatRequirement = (key: string, value: number) => {
const labels: Record<string, string> = {
xp: 'XP Points',
cases: 'Cases',
clients: 'Clients',
proBono: 'Pro Bono Hours',
mentorship: 'Mentorship Sessions',
winRate: 'Win Rate %',
timeRequirement: 'Days Required'
};
return `${labels[key] || key}: ${value}`;
};
const calculateProgress = (requirement: number, current: number) => {
return Math.min((current / requirement) * 100, 100);
};
return (
<div className="bg-white rounded-lg shadow-lg p-6">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">Society of Brothers Progress</h2>
<p className="text-gray-600">Your journey through the 33 degrees of legal mastery</p>
</div>
{/* Navigation Tabs */}
<div className="border-b border-gray-200 mb-6">
<nav className="-mb-px flex space-x-8">
{[
{ key: 'overview', label: 'Overview', icon: FiTrendingUp },
{ key: 'degrees', label: 'Degrees', icon: FiAward },
{ key: 'lodges', label: 'Lodges', icon: FiUsers },
{ key: 'ceremonies', label: 'Ceremonies', icon: FiBookOpen }
].map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => setActiveTab(key as any)}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
activeTab === key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Icon className="w-4 h-4" />
<span>{label}</span>
</button>
))}
</nav>
</div>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Current Degree */}
{currentDegree && (
<div className="bg-gradient-to-r from-blue-50 to-purple-50 p-6 rounded-lg">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="text-4xl">{currentDegree.degree.symbol || '🎖️'}</div>
<div>
<h3 className="text-xl font-bold text-gray-900">
{currentDegree.degree.degreeNumber}° {currentDegree.degree.name}
</h3>
{currentDegree.degree.title && (
<p className="text-purple-600 font-medium">{currentDegree.degree.title}</p>
)}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getLodgeLevelColor(currentDegree.degree.lodgeLevel)}`}>
{currentDegree.degree.lodgeLevel} Lodge
{currentDegree.degree.isSecret && <FiEyeOff className="ml-1 w-3 h-3" />}
</span>
</div>
</div>
<div className="text-right">
<p className="text-sm text-gray-600">Achieved</p>
<p className="font-medium">{format(new Date(currentDegree.achievedAt), 'MMM dd, yyyy')}</p>
{currentDegree.ceremonyCompleted && (
<div className="flex items-center text-green-600 text-sm mt-1">
<FiAward className="w-3 h-3 mr-1" />
Ceremony Complete
</div>
)}
</div>
</div>
<p className="text-gray-700 mb-4">{currentDegree.degree.description}</p>
{/* Privileges */}
<div>
<h4 className="font-medium text-gray-900 mb-2">Privileges Unlocked:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{currentDegree.degree.privileges.map((privilege, index) => (
<div key={index} className="flex items-center text-sm text-green-700">
<FiUnlock className="w-3 h-3 mr-2" />
{privilege}
</div>
))}
</div>
</div>
</div>
)}
{/* Next Degree Progress */}
{nextDegree && showProgress && (
<div className="bg-gray-50 p-6 rounded-lg">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="text-3xl opacity-50">{nextDegree.symbol || '🔒'}</div>
<div>
<h3 className="text-lg font-bold text-gray-900">
Next: {nextDegree.degreeNumber}° {nextDegree.name}
</h3>
{nextDegree.title && (
<p className="text-gray-600">{nextDegree.title}</p>
)}
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-blue-600">{nextDegree.progressPercentage.toFixed(1)}%</div>
<p className="text-sm text-gray-600">Complete</p>
</div>
</div>
{/* Progress Bars */}
<div className="space-y-3">
{Object.entries(nextDegree.requirements).map(([key, required]) => {
if (key === 'timeRequirement' || required === 0) return null;
let current = 0;
switch (key) {
case 'xp': current = userStats.currentXP; break;
case 'cases': current = userStats.totalCases; break;
case 'clients': current = userStats.totalClients; break;
case 'proBono': current = userStats.proBonoHours; break;
case 'mentorship': current = userStats.mentorshipSessions; break;
case 'winRate': current = userStats.winRate; break;
}
const progress = calculateProgress(required, current);
return (
<div key={key}>
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>{formatRequirement(key, required)}</span>
<span>{current} / {required}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
progress >= 100 ? 'bg-green-500' : 'bg-blue-500'
}`}
style={{ width: `${Math.min(progress, 100)}%` }}
></div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* User Statistics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-blue-50 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">{userStats.currentXP.toLocaleString()}</div>
<div className="text-sm text-gray-600">XP Points</div>
</div>
<div className="bg-green-50 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">{userStats.totalCases}</div>
<div className="text-sm text-gray-600">Total Cases</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-600">{userStats.proBonoHours}</div>
<div className="text-sm text-gray-600">Pro Bono Hours</div>
</div>
<div className="bg-yellow-50 p-4 rounded-lg text-center">
<div className="text-2xl font-bold text-yellow-600">
{typeof userStats.winRate === 'number' ? userStats.winRate.toFixed(1) + '%' : 'N/A'}
</div>
<div className="text-sm text-gray-600">Win Rate</div>
</div>
</div>
</div>
)}
{/* Degrees Tab */}
{activeTab === 'degrees' && (
<div className="space-y-4">
{allDegrees.length === 0 ? (
<p className="text-gray-600 text-center py-8">No degrees achieved yet</p>
) : (
allDegrees.map((userDegree) => (
<div key={userDegree.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="text-2xl">{userDegree.degree.symbol || '🎖️'}</div>
<div>
<h3 className="font-semibold text-gray-900">
{userDegree.degree.degreeNumber}° {userDegree.degree.name}
</h3>
{userDegree.degree.title && (
<p className="text-sm" style={{ color: userDegree.degree.color }}>{userDegree.degree.title}</p>
)}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getLodgeLevelColor(userDegree.degree.lodgeLevel)}`}>
{userDegree.degree.lodgeLevel} Lodge
{userDegree.degree.isSecret && <FiEyeOff className="ml-1 w-3 h-3" />}
</span>
</div>
</div>
<div className="text-right">
<p className="text-sm text-gray-600">Achieved</p>
<p className="font-medium">{format(new Date(userDegree.achievedAt), 'MMM dd, yyyy')}</p>
{userDegree.ceremonyCompleted ? (
<div className="flex items-center text-green-600 text-sm mt-1">
<FiAward className="w-3 h-3 mr-1" />
Ceremony Complete
</div>
) : (
<div className="flex items-center text-orange-600 text-sm mt-1">
<FiClock className="w-3 h-3 mr-1" />
Awaiting Ceremony
</div>
)}
</div>
</div>
<p className="text-gray-600 mt-2">{userDegree.degree.description}</p>
</div>
))
)}
</div>
)}
{/* Lodges Tab */}
{activeTab === 'lodges' && (
<div className="space-y-4">
{lodgeMemberships.length === 0 ? (
<p className="text-gray-600 text-center py-8">No lodge memberships</p>
) : (
lodgeMemberships.map((membership, index) => (
<div key={index} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-semibold text-gray-900">{membership.lodge.name}</h3>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getLodgeLevelColor(membership.lodge.lodgeLevel)}`}>
{membership.lodge.lodgeLevel} Lodge
</span>
</div>
<div className="text-right">
<p className="font-medium text-blue-600">{membership.role}</p>
<p className="text-sm text-gray-600">Since {format(new Date(membership.joinedDate), 'MMM yyyy')}</p>
</div>
</div>
<p className="text-gray-600">{membership.lodge.description}</p>
</div>
))
)}
</div>
)}
{/* Ceremonies Tab */}
{activeTab === 'ceremonies' && showCeremonies && (
<div className="space-y-4">
{allDegrees.filter(d => d.ceremonyCompleted).length === 0 ? (
<p className="text-gray-600 text-center py-8">No ceremonies completed yet</p>
) : (
allDegrees
.filter(d => d.ceremonyCompleted)
.map((userDegree) => (
<div key={userDegree.id} className="border border-gray-200 rounded-lg p-4 bg-gradient-to-r from-yellow-50 to-amber-50">
<div className="flex items-center space-x-3 mb-3">
<div className="text-2xl">{userDegree.degree.symbol || '🎖️'}</div>
<div>
<h3 className="font-semibold text-gray-900">
{userDegree.degree.degreeNumber}° {userDegree.degree.name} Ceremony
</h3>
{userDegree.ceremonyDate && (
<p className="text-sm text-gray-600">
Conducted on {format(new Date(userDegree.ceremonyDate), 'MMMM dd, yyyy')}
</p>
)}
</div>
</div>
<div className="bg-white p-3 rounded border-l-4 border-yellow-500">
<p className="text-gray-700 italic">
"By the working tools of an Entered Apprentice, you are received into this ancient and honorable society of legal practitioners."
</p>
</div>
</div>
))
)}
</div>
)}
</div>
);
};
export default SocietyDegreeTracker;