Clean up: Remove src files
This commit is contained in:
parent
f6e4b1de7e
commit
b32b3eaca1
@ -1,202 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import AppShell from './components/common/AppShell';
|
|
||||||
import TaskForm from './components/TaskForm/TaskForm';
|
|
||||||
import TaskList, { FilterCounts } from './components/TaskList/TaskList';
|
|
||||||
import { api } from './services/api';
|
|
||||||
import { Task, TaskDraft, TaskFilter, TaskStatus, User } from './types';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
interface Notice {
|
|
||||||
kind: 'info' | 'error';
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorText = (error: unknown, fallback: string): string =>
|
|
||||||
error instanceof Error && error.message ? error.message : fallback;
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
||||||
const [filter, setFilter] = useState<TaskFilter>('all');
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
|
||||||
const [formOpen, setFormOpen] = useState(false);
|
|
||||||
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
|
||||||
const [pendingTaskId, setPendingTaskId] = useState<number | null>(null);
|
|
||||||
const [notice, setNotice] = useState<Notice | null>(null);
|
|
||||||
|
|
||||||
/** Element that opened the dialog, so focus can go back where the user left it. */
|
|
||||||
const formTriggerRef = useRef<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(async (): Promise<void> => {
|
|
||||||
setLoading(true);
|
|
||||||
setLoadError(null);
|
|
||||||
try {
|
|
||||||
const [me, teamMembers, taskList] = await Promise.all([
|
|
||||||
api.getCurrentUser(),
|
|
||||||
api.getUsers(),
|
|
||||||
api.getTasks(),
|
|
||||||
]);
|
|
||||||
setCurrentUser(me);
|
|
||||||
setUsers(teamMembers);
|
|
||||||
setTasks(taskList);
|
|
||||||
} catch (error) {
|
|
||||||
setLoadError(
|
|
||||||
errorText(error, 'TaskFlow could not reach the server. Check your connection and retry.')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
const counts: FilterCounts = useMemo(
|
|
||||||
() => ({
|
|
||||||
my: tasks.filter((task) => task.assigned_to?.id === currentUser?.id).length,
|
|
||||||
all: tasks.length,
|
|
||||||
blocked: tasks.filter((task) => task.status === 'blocked').length,
|
|
||||||
}),
|
|
||||||
[tasks, currentUser]
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleTasks = useMemo(() => {
|
|
||||||
if (filter === 'my') {
|
|
||||||
return tasks.filter((task) => task.assigned_to?.id === currentUser?.id);
|
|
||||||
}
|
|
||||||
if (filter === 'blocked') {
|
|
||||||
return tasks.filter((task) => task.status === 'blocked');
|
|
||||||
}
|
|
||||||
return tasks;
|
|
||||||
}, [tasks, filter, currentUser]);
|
|
||||||
|
|
||||||
const openForm = (task: Task | null): void => {
|
|
||||||
formTriggerRef.current = document.activeElement as HTMLElement | null;
|
|
||||||
setEditingTask(task);
|
|
||||||
setNotice(null);
|
|
||||||
setFormOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeForm = (): void => {
|
|
||||||
setFormOpen(false);
|
|
||||||
setEditingTask(null);
|
|
||||||
formTriggerRef.current?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Rejects on failure so TaskForm can keep the user's input on screen. */
|
|
||||||
const saveTask = async (draft: TaskDraft): Promise<void> => {
|
|
||||||
if (editingTask) {
|
|
||||||
const updated = await api.updateTask(editingTask.id, draft);
|
|
||||||
setTasks((current) => current.map((task) => (task.id === updated.id ? updated : task)));
|
|
||||||
setNotice({ kind: 'info', text: `"${updated.title}" was updated.` });
|
|
||||||
} else {
|
|
||||||
const created = await api.createTask(draft);
|
|
||||||
setTasks((current) => [created, ...current]);
|
|
||||||
setNotice({ kind: 'info', text: `"${created.title}" was created.` });
|
|
||||||
}
|
|
||||||
closeForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeStatus = async (task: Task, status: TaskStatus): Promise<void> => {
|
|
||||||
setPendingTaskId(task.id);
|
|
||||||
setNotice(null);
|
|
||||||
try {
|
|
||||||
const updated = await api.updateTask(task.id, { status });
|
|
||||||
setTasks((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
|
||||||
setNotice({ kind: 'info', text: `"${updated.title}" is now ${status.replace('_', ' ')}.` });
|
|
||||||
} catch (error) {
|
|
||||||
setNotice({
|
|
||||||
kind: 'error',
|
|
||||||
text: errorText(error, `"${task.title}" could not be updated. It is unchanged, so try again.`),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setPendingTaskId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTask = async (task: Task): Promise<void> => {
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
|
||||||
const confirmed = window.confirm(`Delete "${task.title}"? This cannot be undone.`);
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingTaskId(task.id);
|
|
||||||
setNotice(null);
|
|
||||||
try {
|
|
||||||
await api.deleteTask(task.id);
|
|
||||||
setTasks((current) => current.filter((item) => item.id !== task.id));
|
|
||||||
setNotice({ kind: 'info', text: `"${task.title}" was deleted.` });
|
|
||||||
} catch (error) {
|
|
||||||
setNotice({
|
|
||||||
kind: 'error',
|
|
||||||
text: errorText(error, `"${task.title}" could not be deleted. Nothing changed, so try again.`),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setPendingTaskId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="app-loading" role="status">
|
|
||||||
<div className="loading-spinner" aria-hidden="true" />
|
|
||||||
<p>Loading your team's tasks...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadError || !currentUser) {
|
|
||||||
return (
|
|
||||||
<div className="app-error" role="alert">
|
|
||||||
<h1>We could not load your tasks</h1>
|
|
||||||
<p>{loadError ?? 'Your account details are unavailable right now.'}</p>
|
|
||||||
<button type="button" className="btn btn-primary retry-button" onClick={load}>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell currentUser={currentUser} users={users}>
|
|
||||||
<div className="app-notice-region" aria-live="polite">
|
|
||||||
{notice && (
|
|
||||||
<div className={`app-notice ${notice.kind}`}>
|
|
||||||
<span>{notice.text}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="notice-dismiss"
|
|
||||||
onClick={() => setNotice(null)}
|
|
||||||
aria-label="Dismiss message"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">✕</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TaskList
|
|
||||||
tasks={visibleTasks}
|
|
||||||
totalTaskCount={tasks.length}
|
|
||||||
counts={counts}
|
|
||||||
currentFilter={filter}
|
|
||||||
pendingTaskId={pendingTaskId}
|
|
||||||
onFilterChange={setFilter}
|
|
||||||
onCreateTask={() => openForm(null)}
|
|
||||||
onEditTask={openForm}
|
|
||||||
onDeleteTask={deleteTask}
|
|
||||||
onStatusChange={changeStatus}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formOpen && (
|
|
||||||
<TaskForm users={users} task={editingTask} onCancel={closeForm} onSubmit={saveTask} />
|
|
||||||
)}
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
Loading…
Reference in New Issue
Block a user