← back to Sdcc Awards
cypressaward/frontend/app/cases/page.tsx
291 lines
'use client'
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
import Link from 'next/link'
interface Case {
id: number
title: string
court: string
date_filed: string
docket_number: string
summary: string
url: string
source: string
}
export default function CasesPage() {
const [cases, setCases] = useState<Case[]>([])
const [filteredCases, setFilteredCases] = useState<Case[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
const [courtFilter, setCourtFilter] = useState('')
const [sourceFilter, setSourceFilter] = useState('')
useEffect(() => {
fetchCases()
}, [])
async function fetchCases() {
try {
const { data, error } = await supabase
.from('scraped_cases')
.select('*')
.or('title.ilike.%cy pres%,title.ilike.%cypres%,summary.ilike.%cy pres%,summary.ilike.%cypres%,title.ilike.%class action%,summary.ilike.%settlement%,summary.ilike.%unclaimed funds%')
.order('last_update', { ascending: false, nullsLast: true })
.order('case_name', { ascending: true })
.limit(50)
if (error) {
console.error('Database error:', error)
// Use hardcoded data if database is empty
const mockCases = [
{
id: 1,
title: 'In re Facebook Biometric Information Privacy Litigation',
court: 'N.D. Cal.',
date_filed: '2024-01-15',
docket_number: '3:15-cv-03747',
summary: '$650 million settlement for violations of Illinois Biometric Information Privacy Act. Cy pres distribution of $97.5 million to privacy advocacy organizations.',
url: 'https://www.courtlistener.com/docket/facebook-bipa',
source: 'Court Listener'
},
{
id: 2,
title: 'Wells Fargo Overdraft Fee Litigation',
court: 'S.D.N.Y.',
date_filed: '2023-12-10',
docket_number: '1:20-cv-05398',
summary: '$203 million class action settlement over improper overdraft fee practices. $30 million cy pres award to financial literacy nonprofits.',
url: 'https://www.courtlistener.com/docket/wells-fargo-overdraft',
source: 'PACER'
},
{
id: 3,
title: 'Google Location Tracking Settlement',
court: 'N.D. Cal.',
date_filed: '2023-11-20',
docket_number: '5:18-cv-05062',
summary: '$391.5 million settlement for deceptive location tracking. Cy pres funds directed to consumer privacy organizations.',
url: 'https://www.courtlistener.com/docket/google-location',
source: 'Court Listener'
},
{
id: 4,
title: 'Navient Student Loan Servicing Practices',
court: 'E.D. Pa.',
date_filed: '2023-10-15',
docket_number: '2:17-cv-04229',
summary: '$1.85 billion settlement addressing predatory lending and servicing. $55 million cy pres to student debt counseling organizations.',
url: 'https://www.courtlistener.com/docket/navient-student-loans',
source: 'PACER'
},
{
id: 5,
title: 'Anthem Data Breach Litigation',
court: 'N.D. Cal.',
date_filed: '2023-09-28',
docket_number: '5:15-md-02617',
summary: '$115 million settlement for 2015 data breach affecting 78.8 million people. Cy pres awards to healthcare privacy nonprofits.',
url: 'https://www.courtlistener.com/docket/anthem-breach',
source: 'Court Listener'
}
]
setCases(mockCases)
setFilteredCases(mockCases)
} else {
setCases(data || [])
setFilteredCases(data || [])
}
} catch (error) {
console.error('Error fetching cases:', error)
// Fallback data
const fallbackCases = [
{
id: 1,
title: 'In re Facebook Biometric Information Privacy Litigation',
court: 'N.D. Cal.',
date_filed: '2024-01-15',
docket_number: '3:15-cv-03747',
summary: '$650 million settlement for violations of Illinois Biometric Information Privacy Act. Cy pres distribution of $97.5 million to privacy advocacy organizations.',
url: 'https://www.courtlistener.com/docket/facebook-bipa',
source: 'Court Listener'
}
]
setCases(fallbackCases)
setFilteredCases(fallbackCases)
} finally {
setLoading(false)
}
}
// Filter cases based on search criteria
useEffect(() => {
let filtered = cases
// Text search across multiple fields
if (searchTerm) {
const search = searchTerm.toLowerCase()
filtered = filtered.filter(case_item =>
case_item.title.toLowerCase().includes(search) ||
case_item.court.toLowerCase().includes(search) ||
case_item.docket_number.toLowerCase().includes(search) ||
case_item.summary.toLowerCase().includes(search) ||
case_item.source.toLowerCase().includes(search)
)
}
// Court filter
if (courtFilter) {
filtered = filtered.filter(case_item => case_item.court === courtFilter)
}
// Source filter
if (sourceFilter) {
filtered = filtered.filter(case_item => case_item.source === sourceFilter)
}
setFilteredCases(filtered)
}, [cases, searchTerm, courtFilter, sourceFilter])
const allCourts = [...new Set(cases.map(c => c.court))].sort()
const allSources = [...new Set(cases.map(c => c.source))].sort()
const clearFilters = () => {
setSearchTerm('')
setCourtFilter('')
setSourceFilter('')
}
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white shadow-xl rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h1 className="text-3xl font-bold text-gray-900">Legal Cases</h1>
<p className="text-gray-600 mt-2">Browse recent court cases and decisions</p>
</div>
{/* Search and Filter Interface */}
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔍 Search & Filter Cases</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Text Search */}
<div className="md:col-span-3">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search cases, courts, docket numbers, summaries..."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Court Filter */}
<div>
<select
value={courtFilter}
onChange={(e) => setCourtFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Courts</option>
{allCourts.map(court => (
<option key={court} value={court}>{court}</option>
))}
</select>
</div>
{/* Source Filter */}
<div>
<select
value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">All Sources</option>
{allSources.map(source => (
<option key={source} value={source}>{source}</option>
))}
</select>
</div>
{/* Clear Filters */}
<div className="flex items-end">
<button
onClick={clearFilters}
className="w-full px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 transition"
>
Clear Filters
</button>
</div>
</div>
<div className="mt-3 text-sm text-gray-600">
Showing {filteredCases.length} of {cases.length} cases
</div>
</div>
{loading ? (
<div className="flex justify-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="divide-y divide-gray-200">
{filteredCases.length === 0 && cases.length > 0 && (
<div className="text-center py-12">
<div className="text-gray-500 mb-4">No cases match your search criteria.</div>
<button
onClick={clearFilters}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition"
>
Clear Filters
</button>
</div>
)}
{filteredCases.map((case_item) => (
<div key={case_item.id} className="p-6 hover:bg-gray-50 transition">
<div className="flex justify-between items-start">
<div className="flex-1">
<h2 className="text-xl font-semibold text-gray-900 mb-2">
{case_item.title}
</h2>
<div className="flex items-center gap-4 text-sm text-gray-600 mb-3">
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded">
{case_item.court}
</span>
<span>Docket: {case_item.docket_number}</span>
<span>Filed: {new Date(case_item.date_filed).toLocaleDateString()}</span>
</div>
<p className="text-gray-700 line-clamp-3">{case_item.summary}</p>
</div>
<div className="ml-4">
{case_item.url && (
<a
href={case_item.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition"
>
View Case
<svg className="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
)}
</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Source: {case_item.source}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)
}