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