- Extensive updates to system configuration and deployment - Enhanced documentation and architecture improvements - Updated dependencies and build configurations - Improved service integrations and workflows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
390 lines
16 KiB
TypeScript
390 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Search, ChevronLeft, ChevronRight, ArrowUpDown, Eye } from 'lucide-react'
|
|
import { Lead } from '@/lib/db'
|
|
|
|
interface LeadsTableProps {
|
|
initialLeads: Lead[]
|
|
initialPagination: any
|
|
}
|
|
|
|
export default function LeadsTable({ initialLeads, initialPagination }: LeadsTableProps) {
|
|
const [leads, setLeads] = useState<Lead[]>(initialLeads)
|
|
const [pagination, setPagination] = useState(initialPagination)
|
|
const [loading, setLoading] = useState(false)
|
|
const [search, setSearch] = useState('')
|
|
const [sortBy, setSortBy] = useState('created_at')
|
|
const [sortOrder, setSortOrder] = useState('DESC')
|
|
const [selectedLead, setSelectedLead] = useState<Lead | null>(null)
|
|
|
|
const fetchLeads = async (params: { page?: number; search?: string; sortBy?: string; sortOrder?: string } = {}) => {
|
|
setLoading(true)
|
|
try {
|
|
const searchParams = new URLSearchParams({
|
|
page: (params.page || pagination.page).toString(),
|
|
search: params.search ?? search,
|
|
sortBy: params.sortBy || sortBy,
|
|
sortOrder: params.sortOrder || sortOrder,
|
|
})
|
|
|
|
const response = await fetch(`/api/leads?${searchParams}`)
|
|
const data = await response.json()
|
|
|
|
setLeads(data.leads)
|
|
setPagination(data.pagination)
|
|
} catch (error) {
|
|
console.error('Failed to fetch leads:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSort = (column: string) => {
|
|
const newOrder = sortBy === column && sortOrder === 'ASC' ? 'DESC' : 'ASC'
|
|
setSortBy(column)
|
|
setSortOrder(newOrder)
|
|
fetchLeads({ sortBy: column, sortOrder: newOrder })
|
|
}
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
fetchLeads({ page: 1, search })
|
|
}
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-AU', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="dashboard-card overflow-hidden">
|
|
{/* Header */}
|
|
<div className="p-chorus-lg border-b border-sand-200 dark:border-carbon-700">
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-chorus-md">
|
|
<h3 className="text-xl font-semibold text-carbon-900 dark:text-white">
|
|
Lead Management
|
|
</h3>
|
|
|
|
<form onSubmit={handleSearch} className="flex gap-chorus-sm">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-carbon-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search leads..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="pl-10 pr-4 py-2 border border-sand-300 dark:border-carbon-600 rounded-lg bg-white dark:bg-carbon-800 text-carbon-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-mulberry-500 focus:border-mulberry-500 shadow-sm"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="btn-primary px-chorus-md py-2 text-sm"
|
|
>
|
|
Search
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-sand-100 dark:bg-carbon-800">
|
|
<th className="table-header">
|
|
<button
|
|
onClick={() => handleSort('created_at')}
|
|
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
Date <ArrowUpDown className="h-3 w-3" />
|
|
</button>
|
|
</th>
|
|
<th className="table-header">
|
|
<button
|
|
onClick={() => handleSort('first_name')}
|
|
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
Name <ArrowUpDown className="h-3 w-3" />
|
|
</button>
|
|
</th>
|
|
<th className="table-header">
|
|
<button
|
|
onClick={() => handleSort('email')}
|
|
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
Email <ArrowUpDown className="h-3 w-3" />
|
|
</button>
|
|
</th>
|
|
<th className="table-header">
|
|
<button
|
|
onClick={() => handleSort('company_name')}
|
|
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
Company <ArrowUpDown className="h-3 w-3" />
|
|
</button>
|
|
</th>
|
|
<th className="table-header">
|
|
<button
|
|
onClick={() => handleSort('lead_source')}
|
|
className="flex items-center gap-1 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
Source <ArrowUpDown className="h-3 w-3" />
|
|
</button>
|
|
</th>
|
|
<th className="table-header">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr>
|
|
<td colSpan={6} className="table-cell text-center py-chorus-xxl">
|
|
Loading...
|
|
</td>
|
|
</tr>
|
|
) : leads.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={6} className="table-cell text-center py-chorus-xxl text-carbon-500">
|
|
No leads found
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
leads.map((lead) => (
|
|
<tr key={lead.id} className="hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors">
|
|
<td className="table-cell">
|
|
{formatDate(lead.created_at.toString())}
|
|
</td>
|
|
<td className="table-cell">
|
|
<div>
|
|
<div className="font-medium text-carbon-900 dark:text-white">
|
|
{lead.first_name} {lead.last_name}
|
|
</div>
|
|
{lead.company_role && (
|
|
<div className="text-xs text-carbon-500 dark:text-carbon-400">
|
|
{lead.company_role}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="table-cell">
|
|
<a
|
|
href={`mailto:${lead.email}`}
|
|
className="text-ocean-600 dark:text-ocean-400 hover:underline"
|
|
>
|
|
{lead.email}
|
|
</a>
|
|
</td>
|
|
<td className="table-cell">
|
|
{lead.company_name || '-'}
|
|
</td>
|
|
<td className="table-cell">
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
lead.lead_source === 'request_early_access'
|
|
? 'bg-mulberry-100 text-mulberry-800 dark:bg-mulberry-900 dark:text-mulberry-200'
|
|
: 'bg-ocean-100 text-ocean-800 dark:bg-ocean-900 dark:text-ocean-200'
|
|
}`}>
|
|
{lead.lead_source.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
</span>
|
|
</td>
|
|
<td className="table-cell">
|
|
<button
|
|
onClick={() => setSelectedLead(lead)}
|
|
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-white transition-colors"
|
|
>
|
|
<Eye className="h-4 w-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<div className="p-chorus-lg border-t border-sand-200 dark:border-carbon-700">
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-carbon-600 dark:text-carbon-300">
|
|
Showing {(pagination.page - 1) * pagination.limit + 1} to{' '}
|
|
{Math.min(pagination.page * pagination.limit, pagination.total)} of{' '}
|
|
{pagination.total} results
|
|
</div>
|
|
|
|
<div className="flex items-center gap-chorus-sm">
|
|
<button
|
|
onClick={() => fetchLeads({ page: pagination.page - 1 })}
|
|
disabled={!pagination.hasPrev || loading}
|
|
className="flex items-center gap-1 px-3 py-2 text-sm border border-sand-300 dark:border-carbon-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors text-carbon-700 dark:text-carbon-200"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
Previous
|
|
</button>
|
|
|
|
<span className="text-sm text-carbon-600 dark:text-carbon-300">
|
|
Page {pagination.page} of {pagination.totalPages}
|
|
</span>
|
|
|
|
<button
|
|
onClick={() => fetchLeads({ page: pagination.page + 1 })}
|
|
disabled={!pagination.hasNext || loading}
|
|
className="flex items-center gap-1 px-3 py-2 text-sm border border-sand-300 dark:border-carbon-600 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-sand-100 dark:hover:bg-carbon-800 transition-colors text-carbon-700 dark:text-carbon-200"
|
|
>
|
|
Next
|
|
<ChevronRight className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Lead Detail Modal */}
|
|
{selectedLead && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-chorus-lg z-50">
|
|
<div className="bg-white dark:bg-carbon-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="p-chorus-lg border-b border-sand-200 dark:border-carbon-700">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-xl font-semibold text-carbon-900 dark:text-white">
|
|
Lead Details
|
|
</h3>
|
|
<button
|
|
onClick={() => setSelectedLead(null)}
|
|
className="text-carbon-600 dark:text-carbon-300 hover:text-carbon-900 dark:hover:text-white"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-chorus-lg space-y-chorus-md">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-md">
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Name
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{selectedLead.first_name} {selectedLead.last_name}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Email
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{selectedLead.email}
|
|
</p>
|
|
</div>
|
|
|
|
{selectedLead.company_name && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Company
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{selectedLead.company_name}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedLead.company_role && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Role
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{selectedLead.company_role}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Lead Source
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{selectedLead.lead_source.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Date
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white">
|
|
{formatDate(selectedLead.created_at.toString())}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedLead.inquiry_details && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Inquiry Details
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white whitespace-pre-wrap">
|
|
{selectedLead.inquiry_details}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedLead.custom_message && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-carbon-600 dark:text-carbon-300 mb-1">
|
|
Custom Message
|
|
</label>
|
|
<p className="text-carbon-900 dark:text-white whitespace-pre-wrap">
|
|
{selectedLead.custom_message}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-md text-sm">
|
|
<div>
|
|
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
|
|
IP Address
|
|
</label>
|
|
<p className="text-carbon-700 dark:text-carbon-200">
|
|
{selectedLead.ip_address}
|
|
</p>
|
|
</div>
|
|
|
|
{selectedLead.country_code && (
|
|
<div>
|
|
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
|
|
Country
|
|
</label>
|
|
<p className="text-carbon-700 dark:text-carbon-200">
|
|
{selectedLead.country_code}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
|
|
GDPR Consent
|
|
</label>
|
|
<p className={`${selectedLead.gdpr_consent_given ? 'text-eucalyptus-600' : 'text-coral-600'}`}>
|
|
{selectedLead.gdpr_consent_given ? 'Given' : 'Not Given'}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-carbon-500 dark:text-carbon-400 mb-1">
|
|
Marketing Consent
|
|
</label>
|
|
<p className={`${selectedLead.marketing_consent ? 'text-eucalyptus-600' : 'text-coral-600'}`}>
|
|
{selectedLead.marketing_consent ? 'Given' : 'Not Given'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
} |