227 lines
6.8 KiB
TypeScript
227 lines
6.8 KiB
TypeScript
import React, { useState, useMemo, useTransition, useCallback, useEffect } from 'react';
|
|
|
|
const useDebounce = <T,>(value: T, delay: number): T => {
|
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
|
return () => clearTimeout(handler);
|
|
}, [value, delay]);
|
|
return debouncedValue;
|
|
};
|
|
|
|
interface Toast {
|
|
id: number;
|
|
message: string;
|
|
type: 'success' | 'error' | 'info';
|
|
}
|
|
|
|
const useToast = () => {
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
|
|
const showToast = useCallback((message: string, type: Toast['type'] = 'info') => {
|
|
const id = Date.now();
|
|
setToasts((prev) => [...prev, { id, message, type }]);
|
|
setTimeout(() => {
|
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
}, 3000);
|
|
}, []);
|
|
|
|
return { toasts, showToast };
|
|
};
|
|
|
|
interface Company {
|
|
id: string;
|
|
name: string;
|
|
website: string;
|
|
industry: string;
|
|
employees: number;
|
|
plan: 'free' | 'starter' | 'growth' | 'enterprise';
|
|
contacts: number;
|
|
monthlySpend: number;
|
|
createdAt: string;
|
|
}
|
|
|
|
const mockCompanies: Company[] = [
|
|
{
|
|
id: '1',
|
|
name: 'Acme Corporation',
|
|
website: 'acme.com',
|
|
industry: 'Technology',
|
|
employees: 250,
|
|
plan: 'enterprise',
|
|
contacts: 12,
|
|
monthlySpend: 2500,
|
|
createdAt: '2023-06-15',
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'TechStart Inc',
|
|
website: 'techstart.io',
|
|
industry: 'SaaS',
|
|
employees: 45,
|
|
plan: 'growth',
|
|
contacts: 8,
|
|
monthlySpend: 800,
|
|
createdAt: '2023-09-22',
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Global Solutions',
|
|
website: 'globalsolutions.com',
|
|
industry: 'Consulting',
|
|
employees: 500,
|
|
plan: 'enterprise',
|
|
contacts: 25,
|
|
monthlySpend: 4200,
|
|
createdAt: '2023-03-10',
|
|
},
|
|
];
|
|
|
|
const App: React.FC = () => {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCompany, setSelectedCompany] = useState<Company | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
const { toasts, showToast } = useToast();
|
|
|
|
const debouncedSearch = useDebounce(searchTerm, 300);
|
|
|
|
const stats = useMemo(() => ({
|
|
total: mockCompanies.length,
|
|
enterprise: mockCompanies.filter((c) => c.plan === 'enterprise').length,
|
|
totalContacts: mockCompanies.reduce((sum, c) => sum + c.contacts, 0),
|
|
avgSpend: Math.round(mockCompanies.reduce((sum, c) => sum + c.monthlySpend, 0) / mockCompanies.length),
|
|
}), []);
|
|
|
|
const filteredCompanies = useMemo(() => {
|
|
if (!debouncedSearch) return mockCompanies;
|
|
const term = debouncedSearch.toLowerCase();
|
|
return mockCompanies.filter(
|
|
(c) =>
|
|
c.name.toLowerCase().includes(term) ||
|
|
c.website.toLowerCase().includes(term) ||
|
|
c.industry.toLowerCase().includes(term)
|
|
);
|
|
}, [debouncedSearch]);
|
|
|
|
const handleSearch = (value: string) => {
|
|
startTransition(() => {
|
|
setSearchTerm(value);
|
|
});
|
|
};
|
|
|
|
const handleCompanyClick = (company: Company) => {
|
|
setSelectedCompany(company);
|
|
showToast(`Viewing ${company.name}`, 'info');
|
|
};
|
|
|
|
return (
|
|
<div className="app-container">
|
|
<header className="app-header">
|
|
<h1>Company Directory</h1>
|
|
<p>Browse and manage company accounts</p>
|
|
</header>
|
|
|
|
<div className="stats-grid">
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Companies</div>
|
|
<div className="stat-value">{stats.total}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Enterprise Plans</div>
|
|
<div className="stat-value">{stats.enterprise}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Contacts</div>
|
|
<div className="stat-value">{stats.totalContacts}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Avg Monthly Spend</div>
|
|
<div className="stat-value">${stats.avgSpend}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="search-section">
|
|
<input
|
|
type="text"
|
|
placeholder="Search companies by name, website, or industry..."
|
|
value={searchTerm}
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
className="search-input"
|
|
/>
|
|
{isPending && <div className="search-pending">Searching...</div>}
|
|
</div>
|
|
|
|
{filteredCompanies.length === 0 ? (
|
|
<div className="empty-state">
|
|
<div className="empty-icon">🏢</div>
|
|
<h3>No companies found</h3>
|
|
<p>Try adjusting your search criteria</p>
|
|
</div>
|
|
) : (
|
|
<div className="data-grid">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Company Name</th>
|
|
<th>Website</th>
|
|
<th>Industry</th>
|
|
<th>Employees</th>
|
|
<th>Plan</th>
|
|
<th>Contacts</th>
|
|
<th>Monthly Spend</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredCompanies.map((company) => (
|
|
<tr
|
|
key={company.id}
|
|
onClick={() => handleCompanyClick(company)}
|
|
className={selectedCompany?.id === company.id ? 'selected' : ''}
|
|
>
|
|
<td>{company.name}</td>
|
|
<td>{company.website}</td>
|
|
<td>{company.industry}</td>
|
|
<td>{company.employees}</td>
|
|
<td>
|
|
<span className={`plan-badge plan-${company.plan}`}>
|
|
{company.plan}
|
|
</span>
|
|
</td>
|
|
<td>{company.contacts}</td>
|
|
<td>${company.monthlySpend.toLocaleString()}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{selectedCompany && (
|
|
<div className="detail-panel">
|
|
<h3>Company Details</h3>
|
|
<div className="detail-content">
|
|
<p><strong>Name:</strong> {selectedCompany.name}</p>
|
|
<p><strong>Website:</strong> {selectedCompany.website}</p>
|
|
<p><strong>Industry:</strong> {selectedCompany.industry}</p>
|
|
<p><strong>Employees:</strong> {selectedCompany.employees}</p>
|
|
<p><strong>Plan:</strong> {selectedCompany.plan}</p>
|
|
<p><strong>Contacts:</strong> {selectedCompany.contacts}</p>
|
|
<p><strong>Monthly Spend:</strong> ${selectedCompany.monthlySpend}</p>
|
|
<p><strong>Created:</strong> {selectedCompany.createdAt}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="toast-container">
|
|
{toasts.map((toast) => (
|
|
<div key={toast.id} className={`toast toast-${toast.type}`}>
|
|
{toast.message}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|