195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
import React, { useState, useMemo, useTransition, useEffect, useCallback } from 'react';
|
|
|
|
interface Task {
|
|
id: string;
|
|
title: string;
|
|
assignee: string;
|
|
associatedWith: string;
|
|
associationType: 'contact' | 'deal' | 'company';
|
|
dueDate: string;
|
|
priority: 'low' | 'medium' | 'high';
|
|
status: 'pending' | 'in-progress' | 'completed';
|
|
}
|
|
|
|
const mockTasks: Task[] = [
|
|
{ id: '1', title: 'Follow up call', assignee: 'Sarah Johnson', associatedWith: 'John Doe', associationType: 'contact', dueDate: '2024-02-14', priority: 'high', status: 'pending' },
|
|
{ id: '2', title: 'Send proposal', assignee: 'Mike Chen', associatedWith: 'Acme Corp Deal', associationType: 'deal', dueDate: '2024-02-15', priority: 'high', status: 'in-progress' },
|
|
{ id: '3', title: 'Schedule demo', assignee: 'Tom Brown', associatedWith: 'TechCo', associationType: 'company', dueDate: '2024-02-16', priority: 'medium', status: 'pending' },
|
|
{ id: '4', title: 'Review contract', assignee: 'Sarah Johnson', associatedWith: 'Enterprise Deal', associationType: 'deal', dueDate: '2024-02-13', priority: 'high', status: 'in-progress' },
|
|
{ id: '5', title: 'Quarterly check-in', assignee: 'Mike Chen', associatedWith: 'Jane Smith', associationType: 'contact', dueDate: '2024-02-20', priority: 'low', status: 'pending' },
|
|
];
|
|
|
|
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 [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
const { toasts, addToast } = useToast();
|
|
|
|
const debouncedSearch = useDebounce(searchTerm, 300);
|
|
|
|
const filteredTasks = useMemo(() => {
|
|
if (!debouncedSearch) return mockTasks;
|
|
const term = debouncedSearch.toLowerCase();
|
|
return mockTasks.filter(t =>
|
|
t.title.toLowerCase().includes(term) ||
|
|
t.assignee.toLowerCase().includes(term) ||
|
|
t.associatedWith.toLowerCase().includes(term)
|
|
);
|
|
}, [debouncedSearch]);
|
|
|
|
const stats = useMemo(() => ({
|
|
total: mockTasks.length,
|
|
pending: mockTasks.filter(t => t.status === 'pending').length,
|
|
inProgress: mockTasks.filter(t => t.status === 'in-progress').length,
|
|
high: mockTasks.filter(t => t.priority === 'high').length,
|
|
}), []);
|
|
|
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
startTransition(() => {
|
|
setSearchTerm(e.target.value);
|
|
});
|
|
};
|
|
|
|
const handleSelectTask = (task: Task) => {
|
|
setSelectedTask(task);
|
|
addToast(`Viewing task: ${task.title}`, 'info');
|
|
};
|
|
|
|
return (
|
|
<div className="app">
|
|
<header className="app-header">
|
|
<h1>Task Manager</h1>
|
|
<p>Tasks across deals/contacts, due dates, assignments</p>
|
|
</header>
|
|
|
|
<div className="stats-grid">
|
|
<div className="stat-card">
|
|
<div className="stat-label">Total Tasks</div>
|
|
<div className="stat-value">{stats.total}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">Pending</div>
|
|
<div className="stat-value">{stats.pending}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">In Progress</div>
|
|
<div className="stat-value">{stats.inProgress}</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="stat-label">High Priority</div>
|
|
<div className="stat-value">{stats.high}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="search-bar">
|
|
<input
|
|
type="text"
|
|
placeholder="Search tasks..."
|
|
value={searchTerm}
|
|
onChange={handleSearch}
|
|
className="search-input"
|
|
/>
|
|
{isPending && <span className="search-loading">Searching...</span>}
|
|
</div>
|
|
|
|
{filteredTasks.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No tasks found</h3>
|
|
<p>Try adjusting your search criteria</p>
|
|
</div>
|
|
) : (
|
|
<div className="data-grid">
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Title</th>
|
|
<th>Assignee</th>
|
|
<th>Associated With</th>
|
|
<th>Type</th>
|
|
<th>Due Date</th>
|
|
<th>Priority</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredTasks.map(task => (
|
|
<tr
|
|
key={task.id}
|
|
onClick={() => handleSelectTask(task)}
|
|
className={selectedTask?.id === task.id ? 'selected' : ''}
|
|
>
|
|
<td>{task.title}</td>
|
|
<td>{task.assignee}</td>
|
|
<td>{task.associatedWith}</td>
|
|
<td>{task.associationType}</td>
|
|
<td>{task.dueDate}</td>
|
|
<td>
|
|
<span className={`priority-badge priority-${task.priority}`}>
|
|
{task.priority}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span className={`status-badge status-${task.status}`}>
|
|
{task.status}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTask && (
|
|
<div className="detail-panel">
|
|
<h3>Task Details</h3>
|
|
<div className="detail-grid">
|
|
<div><strong>Title:</strong> {selectedTask.title}</div>
|
|
<div><strong>Assignee:</strong> {selectedTask.assignee}</div>
|
|
<div><strong>Associated With:</strong> {selectedTask.associatedWith}</div>
|
|
<div><strong>Type:</strong> {selectedTask.associationType}</div>
|
|
<div><strong>Due Date:</strong> {selectedTask.dueDate}</div>
|
|
<div><strong>Priority:</strong> {selectedTask.priority}</div>
|
|
<div><strong>Status:</strong> {selectedTask.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;
|