Clean up: Remove component files
This commit is contained in:
parent
9fa73fc1f5
commit
f255414f34
@ -1,223 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Task, TaskFilter, TaskStatus } from '../../types';
|
||||
import TaskCard from './TaskCard';
|
||||
import TaskRow from './TaskRow';
|
||||
import './TaskList.css';
|
||||
|
||||
export interface FilterCounts {
|
||||
my: number;
|
||||
all: number;
|
||||
blocked: number;
|
||||
}
|
||||
|
||||
interface TaskListProps {
|
||||
tasks: Task[];
|
||||
totalTaskCount: number;
|
||||
counts: FilterCounts;
|
||||
currentFilter: TaskFilter;
|
||||
pendingTaskId: number | null;
|
||||
onFilterChange: (filter: TaskFilter) => void;
|
||||
onCreateTask: () => void;
|
||||
onEditTask: (task: Task) => void;
|
||||
onDeleteTask: (task: Task) => void;
|
||||
onStatusChange: (task: Task, status: TaskStatus) => void;
|
||||
}
|
||||
|
||||
const FILTERS: { value: TaskFilter; label: string }[] = [
|
||||
{ value: 'my', label: 'My Tasks' },
|
||||
{ value: 'all', label: 'All Tasks' },
|
||||
{ value: 'blocked', label: 'Blocked' },
|
||||
];
|
||||
|
||||
const COLUMNS = ['Priority', 'Task Name', 'Assignee', 'Status', 'Due Date', 'Actions'];
|
||||
|
||||
const EMPTY_STATES: Record<TaskFilter, { icon: string; heading: string; body: string }> = {
|
||||
my: {
|
||||
icon: '\u2705',
|
||||
heading: 'Nothing assigned to you',
|
||||
body: 'When a teammate assigns you a task it shows up here.',
|
||||
},
|
||||
all: {
|
||||
icon: '\u{1F4CB}',
|
||||
heading: 'No tasks yet',
|
||||
body: 'Get started by creating your first task.',
|
||||
},
|
||||
blocked: {
|
||||
icon: '\u{1F6AB}',
|
||||
heading: 'No blocked tasks',
|
||||
body: 'Your team has no blocked work right now.',
|
||||
},
|
||||
};
|
||||
|
||||
const TaskList: React.FC<TaskListProps> = ({
|
||||
tasks,
|
||||
totalTaskCount,
|
||||
counts,
|
||||
currentFilter,
|
||||
pendingTaskId,
|
||||
onFilterChange,
|
||||
onCreateTask,
|
||||
onEditTask,
|
||||
onDeleteTask,
|
||||
onStatusChange,
|
||||
}) => {
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Left/Right arrows move between filter tabs, as expected of an ARIA tablist.
|
||||
const handleTabKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
const currentIndex = FILTERS.findIndex((filter) => filter.value === currentFilter);
|
||||
const step = event.key === 'ArrowRight' ? 1 : -1;
|
||||
const next = FILTERS[(currentIndex + step + FILTERS.length) % FILTERS.length];
|
||||
|
||||
onFilterChange(next.value);
|
||||
tabsRef.current?.querySelector<HTMLButtonElement>(`#filter-tab-${next.value}`)?.focus();
|
||||
};
|
||||
|
||||
const emptyState = EMPTY_STATES[totalTaskCount === 0 ? 'all' : currentFilter];
|
||||
|
||||
return (
|
||||
<div className="task-list-shell">
|
||||
<div className="list-toolbar">
|
||||
<div
|
||||
className="filter-tabs"
|
||||
role="tablist"
|
||||
aria-label="Filter tasks"
|
||||
ref={tabsRef}
|
||||
onKeyDown={handleTabKeyDown}
|
||||
>
|
||||
{FILTERS.map((filter) => (
|
||||
<button
|
||||
key={filter.value}
|
||||
type="button"
|
||||
id={`filter-tab-${filter.value}`}
|
||||
role="tab"
|
||||
className={`filter-tab${currentFilter === filter.value ? ' active' : ''}`}
|
||||
aria-selected={currentFilter === filter.value}
|
||||
aria-controls="task-panel"
|
||||
tabIndex={currentFilter === filter.value ? 0 : -1}
|
||||
/* The count badge is hidden from the name and folded into the
|
||||
label, otherwise the name reads "All Tasks , 2 tasks". */
|
||||
aria-label={`${filter.label}, ${counts[filter.value]} ${
|
||||
counts[filter.value] === 1 ? 'task' : 'tasks'
|
||||
}`}
|
||||
onClick={() => onFilterChange(filter.value)}
|
||||
>
|
||||
{filter.label}
|
||||
<span
|
||||
className={`filter-count${filter.value === 'blocked' ? ' blocked' : ''}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{counts[filter.value]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* aria-label keeps the name stable when the label is hidden in the mobile FAB. */}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary create-task-btn"
|
||||
onClick={onCreateTask}
|
||||
aria-label="Create task"
|
||||
>
|
||||
<span className="create-icon" aria-hidden="true">
|
||||
+
|
||||
</span>
|
||||
<span className="create-text">Create Task</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="task-panel"
|
||||
id="task-panel"
|
||||
role="tabpanel"
|
||||
aria-labelledby={`filter-tab-${currentFilter}`}
|
||||
>
|
||||
{currentFilter === 'blocked' && tasks.length > 0 && (
|
||||
<div className="blocked-notice">
|
||||
<span className="blocked-notice-icon" aria-hidden="true">
|
||||
⚠
|
||||
</span>
|
||||
<div>
|
||||
<h3>These tasks are blocked</h3>
|
||||
<p>
|
||||
Blocked work cannot move forward yet, usually because it depends on another
|
||||
unfinished task or is waiting on access. Open a task to review the details, then
|
||||
reach out to the assignee.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<div className="task-list-empty">
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon" aria-hidden="true">
|
||||
{emptyState.icon}
|
||||
</div>
|
||||
<h2>{emptyState.heading}</h2>
|
||||
<p>{emptyState.body}</p>
|
||||
{totalTaskCount === 0 && (
|
||||
<button type="button" className="btn btn-primary" onClick={onCreateTask}>
|
||||
Create Task
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="task-list-container">
|
||||
{/* Both views are rendered; CSS media queries decide which one is shown,
|
||||
so only one reaches the accessibility tree at any width. */}
|
||||
<div className="task-table" role="table" aria-label="Tasks">
|
||||
<div className="task-table-header" role="row">
|
||||
{COLUMNS.map((column) => (
|
||||
<div
|
||||
key={column}
|
||||
className={`header-cell ${column.toLowerCase().replace(' ', '-')}`}
|
||||
role="columnheader"
|
||||
>
|
||||
{/* The mockup leaves the priority heading blank. Hiding the
|
||||
text (not the cell) keeps the grid columns aligned. */}
|
||||
<span className={column === 'Priority' ? 'sr-only' : undefined}>{column}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="task-table-body" role="rowgroup">
|
||||
{tasks.map((task) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
busy={pendingTaskId === task.id}
|
||||
onEdit={onEditTask}
|
||||
onDelete={onDeleteTask}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="task-cards">
|
||||
{tasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
busy={pendingTaskId === task.id}
|
||||
onEdit={onEditTask}
|
||||
onDelete={onDeleteTask}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskList;
|
||||
Loading…
Reference in New Issue
Block a user