564 lines
20 KiB
TypeScript
564 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Mail, FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
|
import PDFViewer from '../components/PDFViewer';
|
|
|
|
interface Document {
|
|
id: number;
|
|
title: string;
|
|
summary?: string;
|
|
notes?: string;
|
|
displayOrder: number;
|
|
}
|
|
|
|
interface DocketEntry {
|
|
id: number;
|
|
date: string;
|
|
title: string;
|
|
summary: string;
|
|
notes?: string;
|
|
documents: Document[];
|
|
}
|
|
|
|
export default function HomePage() {
|
|
const [docketEntries, setDocketEntries] = useState<DocketEntry[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [expandedEntries, setExpandedEntries] = useState<Set<number>>(new Set());
|
|
const [email, setEmail] = useState('');
|
|
const [subscribing, setSubscribing] = useState(false);
|
|
const [subscribeMessage, setSubscribeMessage] = useState('');
|
|
const [selectedDocument, setSelectedDocument] = useState<{
|
|
id: number;
|
|
title: string;
|
|
url: string;
|
|
} | null>(null);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchDocketEntries();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const checkMobile = () => {
|
|
setIsMobile(window.innerWidth < 768);
|
|
};
|
|
|
|
checkMobile();
|
|
window.addEventListener('resize', checkMobile);
|
|
|
|
return () => window.removeEventListener('resize', checkMobile);
|
|
}, []);
|
|
|
|
const fetchDocketEntries = async () => {
|
|
try {
|
|
const response = await fetch('/api/docket-entries');
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch docket entries');
|
|
}
|
|
const data = await response.json();
|
|
setDocketEntries(data.entries || []);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleEntry = (entryId: number) => {
|
|
const newExpanded = new Set(expandedEntries);
|
|
if (newExpanded.has(entryId)) {
|
|
newExpanded.delete(entryId);
|
|
} else {
|
|
newExpanded.add(entryId);
|
|
}
|
|
setExpandedEntries(newExpanded);
|
|
};
|
|
|
|
const handleSubscribe = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setSubscribing(true);
|
|
setSubscribeMessage('');
|
|
|
|
try {
|
|
const response = await fetch('/api/subscriptions/subscribe', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
setSubscribeMessage('Successfully subscribed! You will receive notifications for new filings.');
|
|
setEmail('');
|
|
} else {
|
|
setSubscribeMessage(data.message || 'Failed to subscribe. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
setSubscribeMessage('Failed to subscribe. Please try again.');
|
|
} finally {
|
|
setSubscribing(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
// Extract date parts directly to avoid timezone conversion issues
|
|
const datePart = dateString.split('T')[0]; // Get just YYYY-MM-DD part
|
|
if (!datePart) return 'Invalid Date';
|
|
|
|
const parts = datePart.split('-');
|
|
if (parts.length !== 3) return 'Invalid Date';
|
|
|
|
const [year, month, day] = parts;
|
|
if (!year || !month || !day) return 'Invalid Date';
|
|
|
|
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
};
|
|
|
|
const getLastUpdated = () => {
|
|
if (docketEntries.length === 0) return 'No entries';
|
|
const latest = docketEntries.length > 0 ? docketEntries[docketEntries.length - 1] : null;
|
|
return latest ? formatDate(latest.date) : 'No entries';
|
|
};
|
|
|
|
const handleDocumentClick = (doc: Document) => {
|
|
const documentUrl = `/api/documents/${doc.id}/download`;
|
|
|
|
if (isMobile) {
|
|
// On mobile, open PDF directly in new tab (MCDI approach)
|
|
window.open(documentUrl, '_blank');
|
|
} else {
|
|
// On desktop, use modal viewer
|
|
setSelectedDocument({
|
|
id: doc.id,
|
|
title: doc.title,
|
|
url: documentUrl
|
|
});
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
|
<div className="text-center">
|
|
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
|
<p className="text-gray-600">Loading court documents...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{
|
|
width: '100%',
|
|
display: 'block',
|
|
textAlign: 'center',
|
|
margin: '0 auto',
|
|
backgroundColor: '#6E6362',
|
|
minHeight: '100vh'
|
|
}}>
|
|
{/* Centered Header */}
|
|
<header style={{
|
|
width: '100%',
|
|
padding: '2rem 0',
|
|
backgroundColor: 'rgba(57, 64, 83, 0.95)',
|
|
borderBottom: '1px solid #4E4A59',
|
|
textAlign: 'center',
|
|
display: 'block'
|
|
}}>
|
|
<div style={{
|
|
width: '100%',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
margin: '0 auto'
|
|
}}>
|
|
<h1 style={{
|
|
fontSize: '2.5rem',
|
|
fontWeight: 'bold',
|
|
color: 'white',
|
|
marginBottom: '0.5rem',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%',
|
|
lineHeight: '1.2'
|
|
}}>
|
|
ELIZABETH KRAGH<br />
|
|
v.<br />
|
|
MONTANA ASSOCIATION OF THE DEAF
|
|
</h1>
|
|
<p style={{
|
|
color: '#7CAE7A',
|
|
marginBottom: '2rem',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>Court Docket & Legal Documents</p>
|
|
|
|
<div style={{
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>
|
|
<h3 style={{
|
|
fontSize: '1.125rem',
|
|
fontWeight: '500',
|
|
color: 'white',
|
|
marginBottom: '1rem',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>Stay Informed</h3>
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
marginBottom: '2rem',
|
|
width: '100%'
|
|
}}>
|
|
<form onSubmit={handleSubscribe} style={{ display: 'flex', gap: '0.5rem', maxWidth: '28rem' }}>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="Enter your email address"
|
|
style={{
|
|
flex: '1',
|
|
padding: '0.5rem 0.75rem',
|
|
fontSize: '0.875rem',
|
|
border: '1px solid #d1d5db',
|
|
borderRadius: '0.375rem'
|
|
}}
|
|
required
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={subscribing}
|
|
style={{
|
|
backgroundColor: '#7CAE7A',
|
|
color: 'white',
|
|
padding: '0.5rem 1rem',
|
|
borderRadius: '0.375rem',
|
|
fontSize: '0.875rem',
|
|
fontWeight: '500',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
border: 'none',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
<Mail className="w-4 h-4" />
|
|
{subscribing ? 'Subscribing...' : 'Subscribe to Updates'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{subscribeMessage && (
|
|
<p style={{
|
|
marginTop: '0.5rem',
|
|
fontSize: '0.875rem',
|
|
textAlign: 'center',
|
|
color: subscribeMessage.includes('Successfully') ? '#059669' : '#dc2626'
|
|
}}>
|
|
{subscribeMessage}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Centered Case Information */}
|
|
<section style={{
|
|
width: '100%',
|
|
padding: '2rem 0',
|
|
textAlign: 'center',
|
|
display: 'block'
|
|
}}>
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
width: '100%'
|
|
}}>
|
|
<div style={{
|
|
backgroundColor: 'white',
|
|
border: '1px solid #e5e7eb',
|
|
borderRadius: '0.5rem',
|
|
padding: '1.5rem',
|
|
textAlign: 'center',
|
|
maxWidth: '28rem',
|
|
margin: '0 auto'
|
|
}}>
|
|
<h2 style={{
|
|
fontSize: '1.25rem',
|
|
fontWeight: '600',
|
|
color: '#111827',
|
|
marginBottom: '1.5rem',
|
|
textAlign: 'center',
|
|
width: '100%'
|
|
}}>Case Information</h2>
|
|
|
|
<div style={{ textAlign: 'center', marginBottom: '1rem' }}>
|
|
<div style={{ fontSize: '0.875rem', fontWeight: '500', color: '#6b7280', marginBottom: '0.25rem', textAlign: 'center' }}>Case Title</div>
|
|
<div style={{ fontSize: '1rem', fontWeight: '500', color: '#111827', textAlign: 'center' }}>Elizabeth Kragh v. Montana Association of the Deaf</div>
|
|
</div>
|
|
|
|
<div style={{ textAlign: 'center', marginBottom: '1rem' }}>
|
|
<div style={{ fontSize: '0.875rem', fontWeight: '500', color: '#6b7280', marginBottom: '0.25rem', textAlign: 'center' }}>Status</div>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<span style={{
|
|
display: 'inline-block',
|
|
padding: '0.25rem 0.75rem',
|
|
borderRadius: '9999px',
|
|
fontSize: '0.875rem',
|
|
fontWeight: '500',
|
|
backgroundColor: '#dcfce7',
|
|
color: '#166534',
|
|
textAlign: 'center'
|
|
}}>
|
|
Active Litigation
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
<div style={{ fontSize: '0.875rem', fontWeight: '500', color: '#6b7280', marginBottom: '0.25rem', textAlign: 'center' }}>Last Updated</div>
|
|
<div style={{ fontSize: '1rem', fontWeight: '500', color: '#111827', textAlign: 'center' }}>{getLastUpdated()}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Centered Court Docket Entries */}
|
|
<section style={{
|
|
width: '100%',
|
|
padding: '2rem 0',
|
|
textAlign: 'center',
|
|
display: 'block'
|
|
}}>
|
|
<div style={{
|
|
textAlign: 'center',
|
|
width: '100%'
|
|
}}>
|
|
<h2 style={{
|
|
fontSize: '1.5rem',
|
|
fontWeight: '600',
|
|
color: 'white',
|
|
marginBottom: '0.5rem',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>Court Docket Entries</h2>
|
|
<p style={{
|
|
color: 'white',
|
|
marginBottom: '2rem',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>Chronological record of all court filings and proceedings</p>
|
|
|
|
{error ? (
|
|
<div className="bg-white border border-gray-200 rounded-lg p-8 text-center max-w-md">
|
|
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-red-50 flex items-center justify-center">
|
|
<FileText className="w-6 h-6 text-red-500" />
|
|
</div>
|
|
<p className="text-red-600 font-medium mb-4">{error}</p>
|
|
<button
|
|
onClick={fetchDocketEntries}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
) : docketEntries.length === 0 ? (
|
|
<div className="bg-white border border-gray-200 rounded-lg p-12 text-center max-w-md">
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-50 flex items-center justify-center">
|
|
<FileText className="w-8 h-8 text-gray-300" />
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Docket Entries</h3>
|
|
<p className="text-gray-600">No court documents have been filed yet.</p>
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
padding: '0 1rem'
|
|
}}>
|
|
{docketEntries.map((entry, index) => (
|
|
<div
|
|
key={entry.id}
|
|
style={{
|
|
width: '75%',
|
|
maxWidth: '800px',
|
|
minHeight: '120px',
|
|
marginBottom: '8px',
|
|
backgroundColor: 'white',
|
|
border: '2px solid #d1d5db',
|
|
borderRadius: '0.5rem',
|
|
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
|
overflow: 'hidden'
|
|
}}
|
|
>
|
|
<button
|
|
onClick={() => toggleEntry(entry.id)}
|
|
style={{
|
|
width: '100%',
|
|
height: '120px',
|
|
padding: '1.5rem 1.5rem 1rem 1.5rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
backgroundColor: 'transparent',
|
|
border: 'none',
|
|
textAlign: 'left',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.2s'
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flex: 1 }}>
|
|
<div style={{
|
|
width: '2rem',
|
|
height: '2rem',
|
|
borderRadius: '50%',
|
|
backgroundColor: '#394053',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: 'white',
|
|
fontWeight: 'bold',
|
|
fontSize: '0.875rem',
|
|
flexShrink: 0
|
|
}}>
|
|
{index + 1}
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ marginBottom: '0.5rem' }}>
|
|
<div style={{ fontSize: '0.875rem', color: '#6b7280', marginBottom: '0.25rem' }}>
|
|
{formatDate(entry.date)}
|
|
</div>
|
|
</div>
|
|
<h3 style={{
|
|
fontSize: '1rem',
|
|
fontWeight: '500',
|
|
color: '#111827',
|
|
lineHeight: '1.5',
|
|
overflow: 'hidden',
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 2,
|
|
WebkitBoxOrient: 'vertical'
|
|
}}>
|
|
{entry.title || 'No court filing title available.'}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
<div style={{ flexShrink: 0, marginLeft: '1rem' }}>
|
|
{expandedEntries.has(entry.id) ? (
|
|
<ChevronUp style={{ width: '1.25rem', height: '1.25rem', color: '#9ca3af' }} />
|
|
) : (
|
|
<ChevronDown style={{ width: '1.25rem', height: '1.25rem', color: '#9ca3af' }} />
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{/* Expanded Content */}
|
|
{expandedEntries.has(entry.id) && (
|
|
<div className="px-6 pb-6 border-t border-gray-100">
|
|
{entry.summary && (
|
|
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
|
<p className="text-sm text-blue-900 font-medium">Brief Summary:</p>
|
|
<p className="text-sm text-blue-900 mt-1">{entry.summary}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6">
|
|
<div className="space-y-3">
|
|
{entry.documents
|
|
.sort((a, b) => a.displayOrder - b.displayOrder)
|
|
.map((doc) => (
|
|
<div key={doc.id} className="flex items-center justify-center p-3 bg-gray-50 rounded-lg">
|
|
<button
|
|
onClick={() => handleDocumentClick(doc)}
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
padding: '0.375rem 0.75rem',
|
|
backgroundColor: '#7CAE7A',
|
|
color: 'white',
|
|
borderRadius: '0.375rem',
|
|
fontSize: '0.875rem',
|
|
fontWeight: '500',
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.2s'
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#6b9c69'}
|
|
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#7CAE7A'}
|
|
>
|
|
<FileText style={{ width: '1rem', height: '1rem', marginRight: '0.25rem' }} />
|
|
View PDF
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* PDF Viewer Modal */}
|
|
{selectedDocument && (
|
|
<PDFViewer
|
|
isOpen={!!selectedDocument}
|
|
onClose={() => setSelectedDocument(null)}
|
|
documentTitle={selectedDocument.title}
|
|
documentUrl={selectedDocument.url}
|
|
/>
|
|
)}
|
|
|
|
{/* Footer */}
|
|
<footer style={{
|
|
width: '100%',
|
|
padding: '2rem 0',
|
|
backgroundColor: 'rgba(57, 64, 83, 0.95)',
|
|
borderTop: '1px solid #4E4A59',
|
|
textAlign: 'center',
|
|
display: 'block'
|
|
}}>
|
|
<div style={{
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
width: '100%'
|
|
}}>
|
|
<span style={{ color: '#9ca3af' }}>Designed by </span>
|
|
<a
|
|
href="http://deafgain.org"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
color: '#fbbf24',
|
|
textDecoration: 'none',
|
|
transition: 'color 0.2s'
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.color = '#f59e0b'}
|
|
onMouseLeave={(e) => e.currentTarget.style.color = '#fbbf24'}
|
|
>
|
|
DeafGain LLC
|
|
</a>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|