![]() 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 { Calendar, MapPin, Users, AlertCircle, CheckCircle, Clock, Building, Scale, FileText } from 'lucide-react';
interface LegalCase {
id: string;
title: string;
description: string;
caseNumber?: string;
caseType: string;
jurisdiction: string;
court?: string;
firmName?: string;
priority: string;
applicationDeadline?: string;
requiredDocuments: string[];
eligibilityCriteria: any;
logoUrl?: string;
leadLawyer: {
name: string;
title?: string;
specialization?: string;
lawFirm?: {
name: string;
shortName?: string;
address: string;
city: string;
province: string;
};
};
applicationStats: {
totalApplications: number;
isAcceptingApplications: boolean;
};
caseUpdates: Array<{
id: string;
title: string;
description: string;
updateType: string;
createdAt: string;
}>;
}
interface CaseSelectionProps {
selectedCaseId?: string;
onCaseSelect: (caseId: string) => void;
onCaseObjectSelect?: (caseObj: LegalCase) => void;
isFrench?: boolean;
className?: string;
}
const CaseSelection: React.FC<CaseSelectionProps> = ({
selectedCaseId,
onCaseSelect,
onCaseObjectSelect,
isFrench = false,
className = ''
}) => {
const [cases, setCases] = useState<LegalCase[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [expandedCase, setExpandedCase] = useState<string | null>(null);
const texts = {
title: isFrench ? 'Sélectionnez un Recours Collectif' : 'Select a Legal Case',
subtitle: isFrench
? 'Choisissez le recours collectif auquel vous souhaitez vous joindre'
: 'Choose the legal case you want to join',
loading: isFrench ? 'Chargement des recours...' : 'Loading cases...',
error: isFrench ? 'Erreur lors du chargement des recours' : 'Error loading cases',
noCase: isFrench ? 'Aucun recours disponible' : 'No cases available',
noCaseDesc: isFrench
? 'Il n\'y a actuellement aucun recours collectif acceptant de nouvelles demandes.'
: 'There are currently no legal cases accepting new applications.',
leadLawyer: isFrench ? 'Avocat principal' : 'Lead Lawyer',
firm: isFrench ? 'Cabinet' : 'Law Firm',
jurisdiction: isFrench ? 'Juridiction' : 'Jurisdiction',
court: isFrench ? 'Tribunal' : 'Court',
caseType: isFrench ? 'Type de recours' : 'Case Type',
applications: isFrench ? 'demandes' : 'applications',
deadline: isFrench ? 'Date limite' : 'Application Deadline',
eligibility: isFrench ? 'Critères d\'éligibilité' : 'Eligibility Criteria',
required: isFrench ? 'Documents requis' : 'Required Documents',
updates: isFrench ? 'Mises à jour récentes' : 'Recent Updates',
showMore: isFrench ? 'Voir plus de détails' : 'Show more details',
showLess: isFrench ? 'Voir moins' : 'Show less',
selectCase: isFrench ? 'Choisir ce recours' : 'Select this case',
selected: isFrench ? 'Sélectionné' : 'Selected'
};
useEffect(() => {
fetchCases();
}, []);
useEffect(() => {
// If selectedCaseId is provided and not in the list, fetch it directly
if (
selectedCaseId &&
!cases.some((c) => c.id === selectedCaseId)
) {
fetch(`/api/public/cases/${selectedCaseId}`)
.then((res) => res.ok ? res.json() : null)
.then((caseData) => {
const singleCase = caseData?.case || caseData;
if (singleCase && singleCase.id) {
setCases((prev) => {
// Check if the case is already in the array to prevent duplicates
const exists = prev.some(c => c.id === singleCase.id);
return exists ? prev : [...prev, singleCase];
});
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCaseId]);
const fetchCases = async () => {
try {
const response = await fetch('/api/public/cases');
if (response.ok) {
const data = await response.json();
setCases(Array.isArray(data.cases) ? data.cases : []);
} else {
setError('Failed to fetch cases');
setCases([]);
}
} catch (err) {
setError('Network error');
setCases([]);
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString(isFrench ? 'fr-FR' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent': return 'text-red-600 bg-red-50 border-red-200';
case 'high': return 'text-orange-600 bg-orange-50 border-orange-200';
case 'medium': return 'text-blue-600 bg-blue-50 border-blue-200';
case 'low': return 'text-gray-600 bg-gray-50 border-gray-200';
default: return 'text-gray-600 bg-gray-50 border-gray-200';
}
};
const formatCaseType = (type: string) => {
return type.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// Deduplicate cases by id before rendering
const uniqueCases = Array.from(new Map(cases.map(c => [c.id, c])).values());
if (loading) {
return (
<div className={`bg-white rounded-lg shadow-sm border p-6 ${className}`}>
<div className="animate-pulse space-y-4">
<div className="h-6 bg-gray-200 rounded w-1/3"></div>
<div className="space-y-3">
<div className="h-20 bg-gray-200 rounded"></div>
<div className="h-20 bg-gray-200 rounded"></div>
</div>
</div>
<p className="text-center text-gray-500 mt-4">{texts.loading}</p>
</div>
);
}
if (error) {
return (
<div className={`bg-white rounded-lg shadow-sm border p-6 ${className}`}>
<div className="text-center">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">{texts.error}</h3>
<p className="text-gray-600">{error}</p>
<button
onClick={fetchCases}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
{isFrench ? 'Réessayer' : 'Retry'}
</button>
</div>
</div>
);
}
if (cases.length === 0) {
return (
<div className={`bg-white rounded-lg shadow-sm border p-6 ${className}`}>
<div className="text-center">
<Scale className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">{texts.noCase}</h3>
<p className="text-gray-600">{texts.noCaseDesc}</p>
</div>
</div>
);
}
if (selectedCaseId) {
const selected = uniqueCases.find(c => c.id === selectedCaseId);
if (selected) {
return (
<div className={`bg-white rounded-lg shadow-sm border ${className}`}>
<div className="p-6">
<div className="mb-2">
<span className="font-bold">You are applying to: {selected.title}</span>
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Locked</span>
</div>
</div>
</div>
);
}
}
return (
<div className={`bg-white rounded-lg shadow-sm border ${className}`}>
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">{texts.title}</h2>
<p className="text-gray-600 mt-1">{texts.subtitle}</p>
</div>
<div className="divide-y divide-gray-200">
{uniqueCases.map((caseItem) => (
<div
key={caseItem.id}
className={`p-6 transition-colors ${
selectedCaseId === caseItem.id
? 'bg-blue-50 border-l-4 border-l-blue-500'
: 'hover:bg-gray-50'
}`}
>
<div className="space-y-4">
{/* Case Header */}
<div className="flex items-start gap-4">
{/* Case Logo */}
<div className="flex-shrink-0">
{caseItem.logoUrl ? (
<img
src={caseItem.logoUrl}
alt={`${caseItem.title} Logo`}
className="w-12 h-12 rounded-lg object-cover border border-gray-200"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500/20 to-indigo-500/20 border border-gray-200 flex items-center justify-center">
<FileText className="w-6 h-6 text-gray-600" />
</div>
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-gray-900">
{caseItem.title}
</h3>
{caseItem.caseNumber && (
<span className="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded">
{caseItem.caseNumber}
</span>
)}
<span className={`text-xs px-2 py-1 rounded-full border ${getPriorityColor(caseItem.priority)}`}>
{caseItem.priority.toUpperCase()}
</span>
</div>
<p className="text-gray-600 mb-3">
{caseItem.description.length > 200 && expandedCase !== caseItem.id
? `${caseItem.description.substring(0, 200)}...`
: caseItem.description
}
{caseItem.description.length > 200 && (
<button
onClick={() => setExpandedCase(expandedCase === caseItem.id ? null : caseItem.id)}
className="text-blue-600 hover:text-blue-800 ml-2 text-sm"
>
{expandedCase === caseItem.id ? texts.showLess : texts.showMore}
</button>
)}
</p>
</div>
<div>
<button
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${selectedCaseId === caseItem.id ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
onClick={() => {
onCaseSelect(caseItem.id);
if (typeof onCaseObjectSelect === 'function') {
onCaseObjectSelect(caseItem);
}
}}
disabled={selectedCaseId === caseItem.id}
>
{selectedCaseId === caseItem.id ? texts.selected : texts.selectCase}
</button>
</div>
</div>
{/* Case Details */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
<div className="flex items-center gap-2 text-gray-600">
<Scale className="h-4 w-4" />
<div>
<span className="font-medium">{texts.leadLawyer}:</span>
<div>{caseItem.leadLawyer.name}</div>
{caseItem.leadLawyer.title && (
<div className="text-xs text-gray-500">{caseItem.leadLawyer.title}</div>
)}
</div>
</div>
<div className="flex items-center gap-2 text-gray-600">
<Building className="h-4 w-4" />
<div>
<span className="font-medium">{texts.firm}:</span>
<div>{caseItem.leadLawyer.lawFirm?.shortName || caseItem.firmName}</div>
{caseItem.leadLawyer.lawFirm?.city && (
<div className="text-xs text-gray-500">
{caseItem.leadLawyer.lawFirm.city}, {caseItem.leadLawyer.lawFirm.province}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 text-gray-600">
<MapPin className="h-4 w-4" />
<div>
<span className="font-medium">{texts.jurisdiction}:</span>
<div>{caseItem.jurisdiction}</div>
{caseItem.court && (
<div className="text-xs text-gray-500">{caseItem.court}</div>
)}
</div>
</div>
<div className="flex items-center gap-2 text-gray-600">
<Clock className="h-4 w-4" />
<div>
<span className="font-medium">{texts.caseType}:</span>
<div>{formatCaseType(caseItem.caseType)}</div>
</div>
</div>
<div className="flex items-center gap-2 text-gray-600">
<Users className="h-4 w-4" />
<div>
<span className="font-medium">{caseItem.applicationStats?.totalApplications || 0}</span> {texts.applications}
</div>
</div>
{caseItem.applicationDeadline && (
<div className="flex items-center gap-2 text-gray-600">
<Calendar className="h-4 w-4" />
<div>
<span className="font-medium">{texts.deadline}:</span>
<div>{formatDate(caseItem.applicationDeadline)}</div>
</div>
</div>
)}
</div>
{/* Expanded Details */}
{expandedCase === caseItem.id && (
<div className="mt-4 pt-4 border-t border-gray-200 space-y-4">
{caseItem.eligibilityCriteria && Object.keys(caseItem.eligibilityCriteria).length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">{texts.eligibility}</h4>
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded">
{JSON.stringify(caseItem.eligibilityCriteria, null, 2)}
</div>
</div>
)}
{caseItem.requiredDocuments && caseItem.requiredDocuments.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">{texts.required}</h4>
<ul className="text-sm text-gray-600 list-disc list-inside">
{caseItem.requiredDocuments.map((doc, index) => (
<li key={index}>{doc.replace(/_/g, ' ').toLowerCase()}</li>
))}
</ul>
</div>
)}
{caseItem.caseUpdates && caseItem.caseUpdates.length > 0 && (
<div>
<h4 className="font-medium text-gray-900 mb-2">{texts.updates}</h4>
<div className="space-y-2">
{caseItem.caseUpdates.slice(0, 2).map((update) => (
<div key={update.id} className="text-sm p-3 bg-blue-50 rounded">
<div className="font-medium text-blue-900">{update.title}</div>
<div className="text-blue-700 mt-1">{update.description}</div>
<div className="text-blue-600 text-xs mt-1">
{formatDate(update.createdAt)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
))}
</div>
</div>
);
};
export default CaseSelection;