187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
import React, { useState, useMemo, useTransition, useEffect, useCallback } from 'react';
|
|
|
|
interface Campaign {
|
|
id: string;
|
|
name: string;
|
|
type: 'email' | 'social' | 'paid' | 'organic';
|
|
impressions: number;
|
|
clicks: number;
|
|
conversions: number;
|
|
spend: number;
|
|
status: 'active' | 'paused' | 'completed';
|
|
}
|
|
|
|
const mockCampaigns: Campaign[] = [
|
|
{ id: '1', name: 'Q1 Product Launch', type: 'email', impressions: 50000, clicks: 2500, conversions: 125, spend: 5000, status: 'active' },
|
|
{ id: '2', name: 'Brand Awareness', type: 'social', impressions: 120000, clicks: 4800, conversions: 240, spend: 3000, status: 'active' },
|
|
{ id: '3', name: 'Google Ads - Tech', type: 'paid', impressions: 80000, clicks: 3200, conversions: 480, spend: 8000, status: 'active' },
|
|
{ id: '4', name: 'Content Marketing', type: 'organic', impressions: 35000, clicks: 1750, conversions: 87, spend: 0, status: 'active' },
|
|
{ id: '5', name: 'Holiday Promo', type: 'email', impressions: 60000, clicks: 3000, conversions: 300, spend: 4000, status: 'completed' },
|
|
];
|
|
|
|
const useDebounce = <T,>(value: T, delay: number): T => {
|
|
const [debouncedValue, setDebouncedValue] = useState(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 addToast = 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, addToast };
|
|
};
|
|
|
|
const App = () => {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCampaign, setSelectedCampaign] = useState<Campaign | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
const { toasts, addToast } = useToast();
|
|
|
|
const debouncedSearch = useDebounce(searchTerm, 300);
|
|
|
|
const filteredCampaigns = useMemo(() => {
|
|
if (!debouncedSearch) return mockCampaigns;
|
|
const term = debouncedSearch.toLowerCase();
|
|
return mockCampaigns.filter(c => c.name.toLowerCase().includes(term));
|
|
}, [debouncedSearch]);
|
|
|
|
const stats = useMemo(() => ({
|
|
total: mockCampaigns.length,
|
|
totalImpressions: mockCampaigns.reduce((sum, c) => sum + c.impressions, 0),
|
|
totalConversions: mockCampaigns.reduce((sum, c) => sum + c.conversions, 0),
|
|
totalSpend: mockCampaigns.reduce((sum, c) => sum + c.spend, 0),
|
|
}), []);
|
|
|
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
startTransition(() => {
|
|
setSearchTerm(e.target.value);
|
|
});
|
|
};
|
|
|
|
const handleSelectCampaign = (campaign: Campaign) => {
|
|
setSelectedCampaign(campaign);
|
|
addToast(`Viewing ${campaign.name}`, 'info');
|
|
};
|
|
|
|
return (
|
|
<div className="app">
|
|
<header className="app-header">
|
|
<h1>Campaign Dashboard</h1>
|
|
<p>Campaign overview, performance metrics</p>
|
|
</header>
|
|
|
|
<div className="stats-grid">
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Campaigns</div>
|
|
<div className="stat-value">{stats.total}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Impressions</div>
|
|
<div className="stat-value">{(stats.totalImpressions / 1000).toFixed(0)}K</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Conversions</div>
|
|
<div className="stat-value">{stats.totalConversions}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Spend</div>
|
|
<div className="stat-value">${(stats.totalSpend / 1000).toFixed(0)}K</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="search-bar">
|
|
<input
|
|
type="text"
|
|
placeholder="Search campaigns..."
|
|
value={searchTerm}
|
|
onChange={handleSearch}
|
|
className="search-input"
|
|
/>
|
|
{isPending && <span className="search-loading">Searching...</span>}
|
|
</div>
|
|
|
|
{filteredCampaigns.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No campaigns found</h3>
|
|
<p>Try adjusting your search criteria</p>
|
|
</div>
|
|
) : (
|
|
<div className="data-grid">
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Impressions</th>
|
|
<th>Clicks</th>
|
|
<th>Conversions</th>
|
|
<th>Spend</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredCampaigns.map(campaign => (
|
|
<tr
|
|
key={campaign.id}
|
|
onClick={() => handleSelectCampaign(campaign)}
|
|
className={selectedCampaign?.id === campaign.id ? 'selected' : ''}
|
|
>
|
|
<td>{campaign.name}</td>
|
|
<td>{campaign.type}</td>
|
|
<td>{campaign.impressions.toLocaleString()}</td>
|
|
<td>{campaign.clicks.toLocaleString()}</td>
|
|
<td>{campaign.conversions}</td>
|
|
<td>${campaign.spend.toLocaleString()}</td>
|
|
<td>
|
|
<span className={`status-badge status-${campaign.status}`}>
|
|
{campaign.status}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{selectedCampaign && (
|
|
<div className="detail-panel">
|
|
<h3>Campaign Details</h3>
|
|
<div className="detail-grid">
|
|
<div><strong>Name:</strong> {selectedCampaign.name}</div>
|
|
<div><strong>Type:</strong> {selectedCampaign.type}</div>
|
|
<div><strong>Impressions:</strong> {selectedCampaign.impressions.toLocaleString()}</div>
|
|
<div><strong>Clicks:</strong> {selectedCampaign.clicks.toLocaleString()}</div>
|
|
<div><strong>Conversions:</strong> {selectedCampaign.conversions}</div>
|
|
<div><strong>Spend:</strong> ${selectedCampaign.spend.toLocaleString()}</div>
|
|
<div><strong>Status:</strong> {selectedCampaign.status}</div>
|
|
</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;
|