diff --git a/index.css b/index.css new file mode 100644 index 0000000..fa10bf9 --- /dev/null +++ b/index.css @@ -0,0 +1,5 @@ +/* @tailwind base; +@tailwind components; +@tailwind utilities; */ + +@import "tailwindcss"; \ No newline at end of file diff --git a/package.json b/package.json index caf6289..8cd844c 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..44d2119 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 5427540..bd387c5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ( -

React Boilerplate

- ) +
+
+ +

+ Task Manager +

+ + + {/* Add new project */} +
+ setNewProjectName(e.target.value)} + /> + +
+ + {/* Select project */} +
+ + +
+ + + {/* Filter controls */} +
+ {/* Status filter */} + + + {/* Created after date filter */} + 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 */} + +
+ + + + +
+
+ ); } diff --git a/src/Components/TaskForm.jsx b/src/Components/TaskForm.jsx new file mode 100644 index 0000000..6d5f588 --- /dev/null +++ b/src/Components/TaskForm.jsx @@ -0,0 +1,66 @@ +export default function TaskForm({ + onSubmit, + input, + setInput, + dueDate, + setDueDate, + category, + setCategory, + projectId, + setProjectId, + projects = [], +}) { + return ( +
+ setInput(e.target.value)} + aria-label="New task" + /> + setDueDate(e.target.value)} + placeholder="Due date" + aria-label="Due date" + className="border rounded px-3 py-2 w-full sm:w-48" + /> + + + +
+ ); +} diff --git a/src/Components/TaskItem.jsx b/src/Components/TaskItem.jsx new file mode 100644 index 0000000..ba7fff6 --- /dev/null +++ b/src/Components/TaskItem.jsx @@ -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 ( +
  • + onToggle(task.id)} + onKeyDown={(e) => e.key === "Enter" && onToggle(task.id)} + > +
    + {task.text} + {task.category && ( + + {task.category} + + )} +
    + {task.dueDate && ( + + (Due: {format(new Date(task.dueDate), "dd/MM/yyyy")}) + + )} + {task.createdAt && ( + + Added: {format(new Date(task.createdAt), "dd MMM yyyy, p")} + + )} +
    + + +
  • + ); +} diff --git a/src/Components/TaskList.jsx b/src/Components/TaskList.jsx new file mode 100644 index 0000000..ee63164 --- /dev/null +++ b/src/Components/TaskList.jsx @@ -0,0 +1,70 @@ +import TaskItem from "./TaskItem"; + +export default function TaskList({ + tasks, + onToggle, + onRemove, + onCompleteAll, + setDarkMode, + darkMode, + projects, +}) { + if (tasks.length === 0) { + return ( +
    + No tasks yet. Start by adding one! +
    + ); + } + + const allCompleted = tasks.every((task) => task.completed); + + // Group tasks by projectId + const groupedTasks = tasks.reduce((groups, task) => { + const key = task.projectId || "none"; + if (!groups[key]) groups[key] = []; + groups[key].push(task); + return groups; + }, {}); + + return ( +
    + {/* Dark mode toggle button */} +
    + +
    + + {/* Complete All button */} +
    + +
    + + {/* Task List */} + +
    + ); +} diff --git a/src/Components/TaskStats.jsx b/src/Components/TaskStats.jsx new file mode 100644 index 0000000..788d066 --- /dev/null +++ b/src/Components/TaskStats.jsx @@ -0,0 +1,12 @@ +export default function TaskStats({ tasks }) { + const total = tasks.length; + const uncompleted = tasks.filter((t) => !t.completed).length; + + return ( +
    + Total: {total} + | + Uncompleted: {uncompleted} +
    + ); +} \ No newline at end of file diff --git a/src/data/useTaskData.js b/src/data/useTaskData.js new file mode 100644 index 0000000..42caebe --- /dev/null +++ b/src/data/useTaskData.js @@ -0,0 +1,100 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +const useTaskStore = create( + persist( + (set, get) => ({ + tasks: [], + projects: [], + + // Add a new task with optional category and projectId + addTask: (text, dueDate, category = "General", projectId = null) => + set((state) => ({ + tasks: [ + ...state.tasks, + { + id: Date.now(), + text, + dueDate, + category, + projectId, + completed: false, + createdAt: new Date().toISOString(), + }, + ], + })), + + // Remove task + removeTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), + + // Toggle task complete + toggleTask: (id) => + set((state) => { + const updatedTasks = state.tasks.map((task) => + task.id === id ? { ...task, completed: !task.completed } : task + ); + + // Optionally mark project as complete if all its tasks are complete + const affectedTask = state.tasks.find((t) => t.id === id); + const projectId = affectedTask?.projectId; + let updatedProjects = state.projects; + + if (projectId) { + const tasksInProject = updatedTasks.filter( + (t) => t.projectId === projectId + ); + const allDone = tasksInProject.every((t) => t.completed); + + updatedProjects = state.projects.map((p) => + p.id === projectId ? { ...p, completed: allDone } : p + ); + } + + return { + tasks: updatedTasks, + projects: updatedProjects, + }; + }), + + // Complete all tasks + completeAllTasks: () => + set((state) => { + const updatedTasks = state.tasks.map((task) => ({ + ...task, + completed: true, + })); + + // Update project completion status + const updatedProjects = state.projects.map((project) => { + const projectTasks = updatedTasks.filter( + (task) => task.projectId === project.id + ); + const allCompleted = projectTasks.every((t) => t.completed); + return { ...project, completed: allCompleted }; + }); + + return { + tasks: updatedTasks, + projects: updatedProjects, + }; + }), + + // Add a new project + addProject: (name) => + set((state) => ({ + projects: [ + ...state.projects, + { id: Date.now(), name, completed: false }, + ], + })), + }), + { + name: "task-storage", // persisted key in localStorage + } + ) +); + +export default useTaskStore; diff --git a/src/index.css b/src/index.css index f7c0aef..2ababe5 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,10 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; -} +/* +@tailwind base; +@tailwind components; +@tailwind utilities; */ + +/* @tailwind base; +@tailwind components; +@tailwind utilities; */ + +@import "tailwindcss"; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9..cbf4411 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { App } from './App.jsx' +import App from './App.jsx' import './index.css' diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..1653668 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'], + darkMode: 'class', // + theme: { + extend: {}, + }, + plugins: [], +}; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index ba24244..87f6aae 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,7 @@ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' - +import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [react(),tailwindcss()] })