Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* @tailwind base;
@tailwind components;
@tailwind utilities; */

@import "tailwindcss";
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@
"preview": "vite preview"
},
"dependencies": {
"date-fns": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.7",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"postcss": "^8.5.4",
"tailwindcss": "^4.1.8",
"vite": "^6.2.0"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};
177 changes: 174 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,176 @@
export const App = () => {
import { useState, useEffect } from "react";
import useTaskStore from "./data/useTaskData";
import TaskForm from "./Components/TaskForm";
import TaskList from "./Components/TaskList";
import TaskStats from "./Components/TaskStats";

export default function App() {
const { tasks, addTask, removeTask, toggleTask, completeAllTasks ,projects, addProject} =
useTaskStore();
const [input, setInput] = useState("");
const [dueDate, setDueDate] = useState(""); // date state
const [darkMode, setDarkMode] = useState(false);

// Filter state
const [filterStatus, setFilterStatus] = useState("all"); // all, completed, uncompleted
const [filterDate, setFilterDate] = useState(""); // yyyy-MM-dd

// new category state
const [category, setCategory] = useState("General");

const [selectedProjectId, setSelectedProjectId] = useState(null);
const [newProjectName, setNewProjectName] = useState("");


useEffect(() => {
const root = window.document.documentElement;
if (darkMode) {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
}, [darkMode]);

// Apply filters to tasks here
const filteredTasks = tasks.filter((task) => {
// Filter by status
if (filterStatus === "completed" && !task.completed) return false;
if (filterStatus === "uncompleted" && task.completed) return false;

// Filter by creation date if date filter set
if (filterDate) {
const filterDateObj = new Date(filterDate);
const taskCreatedDate = new Date(task.createdAt);
if (taskCreatedDate < filterDateObj) return false;
}

return true;
});

const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
addTask(input.trim(), dueDate, category); // pass dueDate to addTask
setInput("");
setCategory("General"); // reset to default after adding
setDueDate("");
};

return (
<h1>React Boilerplate</h1>
)
<main className="min-h-screen bg-gray-100 dark:bg-gray-900 p-4 flex flex-col items-center transition-colors">
<div className="w-full max-w-4xl px-4 sm:px-6 md:px-8">
<button
onClick={() => setDarkMode(!darkMode)}
className="mb-4 px-3 py-2 sm:px-4 sm:py-2 bg-gray-200 dark:bg-gray-700 text-sm sm:text-base rounded"
>
Toggle {darkMode ? "Light" : "Dark"} Mode
</button>
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold mb-4 text-center">
Task Manager
</h1>


{/* Add new project */}
<div className="mb-4 flex gap-2 items-center">
<input
type="text"
placeholder="New Project"
className="border rounded px-3 py-1 dark:bg-gray-700 dark:text-white"
value={newProjectName}
onChange={(e) => setNewProjectName(e.target.value)}
/>
<button
onClick={() => {
if (newProjectName.trim()) {
addProject(newProjectName.trim());
setNewProjectName("");
}
}}
className="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded"
>
Add Project
</button>
</div>

{/* Select project */}
<div className="mb-4">
<label className="mr-2 text-sm font-medium">Project:</label>
<select
value={selectedProjectId || ""}
onChange={(e) =>
setSelectedProjectId(e.target.value ? Number(e.target.value) : null)
}
className="border rounded px-3 py-1 dark:bg-gray-700 dark:text-white"
>
<option value="">All Projects</option>
{projects.map((proj) => (
<option key={proj.id} value={proj.id}>
{proj.name}
</option>
))}
</select>
</div>
<TaskForm
onSubmit={handleSubmit}
input={input}
setInput={setInput}
dueDate={dueDate}
setDueDate={setDueDate}
category={category}
setCategory={setCategory}
/>

{/* Filter controls */}
<div className="flex gap-4 mb-4 items-center justify-between">
{/* Status filter */}
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="border rounded px-3 py-1 dark:bg-gray-700 dark:text-white"
aria-label="Filter tasks by status"
>
<option value="all">All Tasks</option>
<option value="completed">Completed</option>
<option value="uncompleted">Uncompleted</option>
</select>

{/* Created after date filter */}
<input
type="date"
value={filterDate}
onChange={(e) => setFilterDate(e.target.value)}
className="border rounded px-3 py-1 dark:bg-gray-700 dark:text-white"
aria-label="Show tasks created after date"
/>

{/* Clear filters button */}
<button
onClick={() => {
setFilterStatus("all");
setFilterDate("");
}}
className="bg-gray-300 dark:bg-gray-600 px-3 py-1 rounded text-sm"
>
Clear Filters
</button>
</div>

<TaskList
tasks={filteredTasks}
onToggle={toggleTask}
onRemove={removeTask}
onCompleteAll={completeAllTasks}
darkMode={darkMode}
setDarkMode={setDarkMode}
/>
<TaskStats tasks={filteredTasks} />
<button
onClick={completeAllTasks}
className="mt-4 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded w-full sm:w-auto"
>
Complete All
</button>
</div>
</main>
);
}
66 changes: 66 additions & 0 deletions src/Components/TaskForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export default function TaskForm({
onSubmit,
input,
setInput,
dueDate,
setDueDate,
category,
setCategory,
projectId,
setProjectId,
projects = [],
}) {
return (
<form
onSubmit={onSubmit}
className="flex flex-col sm:flex-row sm:items-center gap-2 mb-4 w-full"
>
<input
type="text"
className="border rounded px-3 py-2 flex-1 w-full sm:w-auto"
placeholder="Add a task"
value={input}
onChange={(e) => setInput(e.target.value)}
aria-label="New task"
/>
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
placeholder="Due date"
aria-label="Due date"
className="border rounded px-3 py-2 w-full sm:w-48"
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="border rounded px-3 py-2"
aria-label="Task category"
>
<option value="General">General</option>
<option value="Housework">Housework</option>
<option value="Shopping">Shopping</option>
<option value="Work">Work</option>
</select>
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="border rounded px-3 py-2"
aria-label="Assign to project"
>
<option value="">No Project</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded w-full sm:w-auto"
>
Add
</button>
</form>
);
}
52 changes: 52 additions & 0 deletions src/Components/TaskItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { format, isBefore, startOfDay } from "date-fns";

export default function TaskItem({ task, onToggle, onRemove }) {
const today = startOfDay(new Date());
const isOverdue =
task.dueDate && isBefore(new Date(task.dueDate), today) && !task.completed;

return (
<li className="flex justify-between items-center bg-white dark:bg-gray-700 px-4 py-2 rounded shadow">
<span
className={`flex-1 cursor-pointer ${
task.completed
? "line-through text-gray-400"
: isOverdue
? "text-red-600 font-semibold"
: ""
}`}
role="button"
tabIndex={0}
onClick={() => onToggle(task.id)}
onKeyDown={(e) => e.key === "Enter" && onToggle(task.id)}
>
<div className="flex items-center space-x-2">
<span>{task.text}</span>
{task.category && (
<span className="bg-blue-200 text-blue-800 text-xs px-2 py-0.5 rounded">
{task.category}
</span>
)}
</div>
{task.dueDate && (
<span className="ml-2 text-sm text-gray-500 dark:text-gray-300">
(Due: {format(new Date(task.dueDate), "dd/MM/yyyy")})
</span>
)}
{task.createdAt && (
<span className="block text-xs text-gray-400 mt-1">
Added: {format(new Date(task.createdAt), "dd MMM yyyy, p")}
</span>
)}
</span>

<button
onClick={() => onRemove(task.id)}
className="text-red-600 ml-4"
aria-label={`Delete ${task.text}`}
>
</button>
</li>
);
}
Loading