Files
adopt-a-street/frontend/src/components/TaskList.js
T
William Valentin 2df5a303ed refactor: convert frontend from submodule to true monorepo
Convert frontend from Git submodule to a regular monorepo directory for simplified development workflow.

Changes:
- Remove frontend submodule tracking (mode 160000 gitlink)
- Add all frontend source files directly to main repository
- Remove frontend/.git directory
- Update CLAUDE.md to clarify true monorepo structure
- Update Frontend Architecture documentation (React Router v6, Socket.IO, Leaflet, ErrorBoundary)

Benefits of Monorepo:
- Single git clone for entire project
- Unified commit history
- Simpler CI/CD pipeline
- Easier for new developers
- No submodule sync issues
- Atomic commits across frontend and backend

Frontend Files Added:
- All React components (MapView, ErrorBoundary, TaskList, SocialFeed, etc.)
- Context providers (AuthContext, SocketContext)
- Complete test suite with MSW
- Dependencies and configuration files

Branch Cleanup:
- Using 'main' as default branch (develop deleted)
- Frontend no longer has separate Git history

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 11:01:06 -07:00

214 lines
6.2 KiB
JavaScript

import React, { useState, useEffect, useContext, useCallback } from "react";
import axios from "axios";
import { toast } from "react-toastify";
import { AuthContext } from "../context/AuthContext";
import { SocketContext } from "../context/SocketContext";
/**
* TaskList component displays maintenance tasks and allows task completion
* Includes real-time updates via Socket.IO
*/
const TaskList = () => {
const { auth } = useContext(AuthContext);
const { socket, connected, on, off } = useContext(SocketContext);
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [completingTaskId, setCompletingTaskId] = useState(null);
// Load tasks from API
const loadTasks = useCallback(async () => {
try {
setLoading(true);
setError(null);
const res = await axios.get("/api/tasks");
setTasks(res.data);
} catch (err) {
console.error("Error loading tasks:", err);
const errorMessage =
err.response?.data?.msg ||
err.response?.data?.message ||
"Failed to load tasks. Please try again later.";
setError(errorMessage);
toast.error(errorMessage);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadTasks();
}, [loadTasks]);
// Handle real-time task updates via Socket.IO
useEffect(() => {
if (!socket || !connected) return;
const handleTaskUpdate = (data) => {
console.log("Received task update:", data);
if (data.type === "task_completed") {
// Update specific task status
setTasks((prevTasks) =>
prevTasks.map((task) =>
task._id === data.taskId ? { ...task, status: "completed" } : task
)
);
toast.success("Task completed!");
} else if (data.type === "new_task") {
// Add new task to the list
setTasks((prevTasks) => [data.task, ...prevTasks]);
toast.info("New task available!");
} else if (data.type === "task_updated") {
// Update existing task
setTasks((prevTasks) =>
prevTasks.map((task) =>
task._id === data.task._id ? data.task : task
)
);
} else if (data.type === "task_deleted") {
// Remove deleted task
setTasks((prevTasks) =>
prevTasks.filter((task) => task._id !== data.taskId)
);
}
};
// Subscribe to task updates
on("taskUpdate", handleTaskUpdate);
// Cleanup on unmount
return () => {
off("taskUpdate", handleTaskUpdate);
};
}, [socket, connected, on, off]);
// Complete a task
const completeTask = async (id) => {
if (!auth.isAuthenticated) {
toast.warning("Please login to complete tasks");
return;
}
try {
setCompletingTaskId(id);
const token = localStorage.getItem("token");
const res = await axios.put(
`/api/tasks/${id}`,
{},
{
headers: {
"x-auth-token": token,
},
}
);
setTasks(tasks.map((task) => (task._id === id ? res.data : task)));
toast.success("Task completed successfully!");
} catch (err) {
console.error("Error completing task:", err);
const errorMessage =
err.response?.data?.msg ||
err.response?.data?.message ||
"Failed to complete task. Please try again.";
toast.error(errorMessage);
} finally {
setCompletingTaskId(null);
}
};
if (loading) {
return (
<div className="text-center mt-5">
<div className="spinner-border" role="status">
<span className="sr-only">Loading...</span>
</div>
<p>Loading tasks...</p>
</div>
);
}
if (error) {
return (
<div className="alert alert-danger m-3" role="alert">
<h4 className="alert-heading">Error Loading Tasks</h4>
<p>{error}</p>
<hr />
<button className="btn btn-primary" onClick={loadTasks}>
Retry
</button>
</div>
);
}
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h1>Task List</h1>
{connected && (
<span className="badge badge-success">
<span className="mr-1">&#9679;</span> Live Updates
</span>
)}
</div>
{tasks.length === 0 ? (
<div className="alert alert-info">
No tasks available at the moment. Check back later!
</div>
) : (
<ul className="list-group">
{tasks.map((task) => (
<li
key={task._id}
className="list-group-item d-flex justify-content-between align-items-center"
>
<div>
<strong>{task.description}</strong>
<br />
<span
className={`badge badge-${
task.status === "pending" ? "warning" : "success"
} mr-2`}
>
{task.status}
</span>
{task.street && (
<small className="text-muted">Street: {task.street.name || task.street}</small>
)}
{task.assignedTo && (
<small className="text-muted ml-2">
Assigned to: {task.assignedTo.name || task.assignedTo}
</small>
)}
</div>
{task.status === "pending" && auth.isAuthenticated && (
<button
className="btn btn-sm btn-success"
onClick={() => completeTask(task._id)}
disabled={completingTaskId === task._id}
>
{completingTaskId === task._id ? (
<>
<span
className="spinner-border spinner-border-sm mr-1"
role="status"
aria-hidden="true"
></span>
Completing...
</>
) : (
"Complete"
)}
</button>
)}
</li>
))}
</ul>
)}
</div>
);
};
export default TaskList;