diff --git a/README.md b/README.md
index d1c68b57..4ade84d3 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,25 @@
-# Todo
\ No newline at end of file
+# Todo App (React)
+
+This is a Todo application built with React to demonstrate clean state management, accessibility, and maintainable frontend architecture.
+
+Features
+
+Create, toggle, and remove tasks
+
+Persist tasks using localStorage
+
+Display task count and creation date
+
+Toggle all tasks with a single action
+
+Accessible and keyboard-friendly UI
+
+Responsive layout
+
+Technical Overview
+
+Global state managed with Zustand (no prop-drilling)
+
+State persistence via localStorage
+
+Logic separated into stores, components, and utility modules
diff --git a/package.json b/package.json
index caf62893..f8dd80f9 100644
--- a/package.json
+++ b/package.json
@@ -11,17 +11,22 @@
},
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "styled-components": "^6.3.5",
+ "zustand": "^5.0.10"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.23",
"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.6",
+ "tailwindcss": "^4.1.18",
"vite": "^6.2.0"
}
}
diff --git a/src/App.jsx b/src/App.jsx
index 54275400..54a10c55 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,34 @@
-export const App = () => {
+import { TodoList } from "./components/TodoList";
+import { TodoForm } from "./components/TodoForm";
+import { TodoCount } from "./components/TodoCount";
+import { useTodosStore } from "./stores/useTodosStore";
+import { AppWrapper, Header, BulkButton } from "./styles/App.styles";
+import { GlobalStyle } from "./styles/GlobalStyles";
+
+export default function App() {
+ //Get only the action from the global Zustand store
+ const completeAll = useTodosStore((state) => state.completeAll);
return (
-
React Boilerplate
- )
+ <>
+ {/*Global styles fpr the entire app */}
+
+ {/* Main app container */}
+
+
+ {/* App title */}
+ Todo App
+
+ {/* Form to add new todos */}
+
+ {/* Displays total task */}
+
+ {/* Bulk action to toggle all todos */}
+
+ Select all/deselect all{" "}
+
+ {/* List of all todos */}
+
+
+ >
+ );
}
diff --git a/src/components/TodoCount.jsx b/src/components/TodoCount.jsx
new file mode 100644
index 00000000..523677a6
--- /dev/null
+++ b/src/components/TodoCount.jsx
@@ -0,0 +1,8 @@
+import { useTodosStore } from "../stores/useTodosStore";
+
+export function TodoCount() {
+ // Read todos from the global Zustand store
+ const todos = useTodosStore((state) => state.todos);
+ // Display the total number of tasks
+ return Number of tasks: {todos.length}
;
+}
diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx
new file mode 100644
index 00000000..d017b9db
--- /dev/null
+++ b/src/components/TodoForm.jsx
@@ -0,0 +1,39 @@
+import { useState } from "react";
+import { useTodosStore } from "../stores/useTodosStore";
+import { Form, Input, Submit } from "../styles/TodoForm.styles";
+
+export function TodoForm() {
+ // Local state for the input value
+ const [title, setTitle] = useState("");
+
+ // Get the addTodo action from the global store
+ const addTodo = useTodosStore((state) => state.addTodo);
+
+ function handleSumbit(e) {
+ // Prevent default form submission (page reload)
+ e.preventDefault();
+
+ // Add new todo using global state
+ addTodo(title);
+
+ // Clear the input after submit
+ setTitle("");
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
new file mode 100644
index 00000000..7e4bed87
--- /dev/null
+++ b/src/components/TodoList.jsx
@@ -0,0 +1,56 @@
+import { useTodosStore } from "../stores/useTodosStore";
+import { formatDate } from "../utils/formatDate";
+import {
+ List,
+ Item,
+ TodoButton,
+ RemoveButton,
+ Paragraf,
+} from "../styles/TodoList.styles";
+
+export function TodoList() {
+ // Read state + actions from the global Zustand store (no prop drilling)
+ const todos = useTodosStore((state) => state.todos);
+ const toggleTodo = useTodosStore((state) => state.toggleTodo);
+ const removeTodo = useTodosStore((state) => state.removeTodo);
+
+ // Empty state UX when there are no tasks
+ if (todos.length === 0) {
+ return No tasks yet. Add your first one above. ;
+ }
+
+ return (
+
+ {todos.map((todo) => (
+ -
+ {/* Click the todo to toggle completed/uncompleted */}
+
toggleTodo(todo.id)}
+ aria-pressed={todo.completed}
+ >
+
+ toggleTodo(todo.id)}
+ onClick={(e) => e.stopPropagation()}
+ />
+ {todo.title}
+
+
+ Created: {formatDate(todo.createdAt)}
+
+ {/* Separate remove action with an accessible label */}
+ removeTodo(todo.id)}
+ >
+ ✕ Remove
+
+
+ ))}
+
+ );
+}
diff --git a/src/main.jsx b/src/main.jsx
index 1b8ffe9b..5e424eef 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,12 +1,12 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
+import React from "react";
+import ReactDOM from "react-dom/client";
-import { App } from './App.jsx'
+import App from "./App.jsx";
-import './index.css'
+import "./index.css";
-ReactDOM.createRoot(document.getElementById('root')).render(
+ReactDOM.createRoot(document.getElementById("root")).render(
-)
+);
diff --git a/src/stores/useTodosStore.js b/src/stores/useTodosStore.js
new file mode 100644
index 00000000..ca853e52
--- /dev/null
+++ b/src/stores/useTodosStore.js
@@ -0,0 +1,66 @@
+import { create } from "zustand";
+import { getInitialTodos, saveTodos } from "../utils/todosStorage";
+
+// Global todo store using Zustand
+export const useTodosStore = create((set) => ({
+ // Initial state loaded from local storage
+ todos: getInitialTodos(),
+
+ // Add a new todo item
+ addTodo: (title) => {
+ set((state) => {
+ const newTodos = [
+ ...state.todos,
+ {
+ id: Date.now().toString(), // simple unique id
+ title,
+ completed: false,
+ createdAt: Date.now(),
+ },
+ ];
+
+ // Persist updated todos
+ saveTodos(newTodos);
+ return { todos: newTodos };
+ });
+ },
+
+ // Remove a todo by id
+ removeTodo: (id) => {
+ set((state) => {
+ const newTodos = state.todos.filter((todo) => todo.id !== id);
+
+ // Persist updated todos
+ saveTodos(newTodos);
+ return { todos: newTodos };
+ });
+ },
+
+ // Toggle all todos as completed / uncompleted
+ completeAll: () => {
+ set((state) => {
+ const allCompleted = state.todos.every((todo) => todo.completed);
+ const newTodos = state.todos.map((todo) => ({
+ ...todo,
+ completed: !allCompleted,
+ }));
+
+ // Persist updated todos
+ saveTodos(newTodos);
+ return { todos: newTodos };
+ });
+ },
+
+ // Toggle completed state for a single todo
+ toggleTodo: (id) => {
+ set((state) => {
+ const newTodos = state.todos.map((todo) =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ );
+
+ // Persist updated todos
+ saveTodos(newTodos);
+ return { todos: newTodos };
+ });
+ },
+}));
diff --git a/src/styles/App.styles.js b/src/styles/App.styles.js
new file mode 100644
index 00000000..b44af6f4
--- /dev/null
+++ b/src/styles/App.styles.js
@@ -0,0 +1,45 @@
+import styled from "styled-components";
+import { BaseButton } from "./Buttons.styles";
+
+export const BulkButton = styled(BaseButton)`
+ margin: 12px 0 20px;
+ padding: 10px 14px;
+`;
+
+export const AppWrapper = styled.main`
+ width: min(600px, 100%);
+ max-width: 100%;
+ margin: 30px auto;
+ padding: 0 16px;
+ box-sizing: border-box;
+ min-width: 0;
+
+ overflow-x: clip;
+
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 12px 30px rgba(17, 24, 39, 0.12);
+
+ @media (min-width: 640px) {
+ margin: 40px auto;
+ padding: 40px;
+ }
+`;
+export const Header = styled.header`
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ h1 {
+ padding-top: 15px;
+ padding-bottom: 10px;
+ margin: 0;
+ font-size: 24px;
+ text-align: center;
+ }
+
+ @media (min-width: 640px) {
+ h1 {
+ font-size: 28px;
+ }
+ }
+`;
diff --git a/src/styles/Buttons.styles.js b/src/styles/Buttons.styles.js
new file mode 100644
index 00000000..41479873
--- /dev/null
+++ b/src/styles/Buttons.styles.js
@@ -0,0 +1,40 @@
+import styled from "styled-components";
+
+export const BaseButton = styled.button`
+ border-radius: 10px;
+ padding: 10px 14px;
+
+ background: #f7f8fc;
+ border: 1px solid #d7dbe7;
+ color: #111827;
+
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 1.4;
+
+ cursor: pointer;
+ transition: background-color 0.2s ease, border-color 0.2s ease,
+ transform 0.05s ease, box-shadow 0.2s ease;
+
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ background: #eef1ff;
+ border-color: #2563eb;
+ }
+ }
+
+ &:focus-visible {
+ outline: 3px solid #93c5fd;
+ outline-offset: 2px;
+ border-color: #2563eb;
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+`;
diff --git a/src/styles/GlobalStyles.js b/src/styles/GlobalStyles.js
new file mode 100644
index 00000000..39f33324
--- /dev/null
+++ b/src/styles/GlobalStyles.js
@@ -0,0 +1,19 @@
+import { createGlobalStyle } from "styled-components";
+
+export const GlobalStyle = createGlobalStyle`
+ *, *::before, *::after {
+ box-sizing: border-box;
+ }
+
+ html, body, #root {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ overflow-x: hidden;
+ overscroll-behavior-x: none;
+ }
+
+ body {
+ touch-action: pan-y;
+ }
+`;
diff --git a/src/styles/TodoForm.styles.js b/src/styles/TodoForm.styles.js
new file mode 100644
index 00000000..e050267f
--- /dev/null
+++ b/src/styles/TodoForm.styles.js
@@ -0,0 +1,60 @@
+import styled from "styled-components";
+import { BaseButton } from "./Buttons.styles";
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0px;
+
+ width: 100%;
+ max-width: 100%;
+ overflow: hidden;
+
+ /* Tablet & desktop */
+ @media (min-width: 640px) {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 10px;
+ padding: 24px 5px;
+ align-items: center;
+ max-width: 500px;
+ margin: 0 auto; /
+ }
+
+ label {
+ @media (min-width: 640px) {
+ grid-column: 1 / -1;
+ }
+ }
+`;
+
+export const Input = styled.input`
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+
+ padding: 12px 14px;
+ border: 1px solid #d7dbe7;
+ border-radius: 10px;
+ font-size: 16px;
+ box-sizing: border-box;
+
+ @media (min-width: 640px) {
+ grid-column: 1;
+ }
+
+ &:focus {
+ border-color: #6b8cff;
+ box-shadow: 0 0 0 3px rgba(107, 140, 255, 0.25);
+ }
+`;
+export const Submit = styled(BaseButton)`
+ padding: 12px 18px;
+ width: 100%;
+
+ @media (min-width: 640px) {
+ width: auto;
+ white-space: nowrap;
+ }
+`;
diff --git a/src/styles/TodoList.styles.js b/src/styles/TodoList.styles.js
new file mode 100644
index 00000000..27a757ae
--- /dev/null
+++ b/src/styles/TodoList.styles.js
@@ -0,0 +1,86 @@
+import styled from "styled-components";
+import { BaseButton } from "./Buttons.styles";
+
+export const List = styled.ul`
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ padding-bottom: 20px;
+ padding-top: 20px;
+`;
+
+export const Item = styled.li`
+ display: flex;
+ gap: 10px;
+ align-items: stretch;
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ margin-bottom: 12px;
+`;
+
+export const TodoButton = styled.button`
+ flex: 1 1 auto;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 6px;
+
+ text-align: left;
+ white-space: normal;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+
+ border: 1px solid #d7dbe7;
+ border-radius: 10px;
+ padding: 12px 14px;
+ background: #fff;
+
+ &:focus-visible {
+ outline: 3px solid #93c5fd;
+ outline-offset: 2px;
+ border-color: #2563eb;
+ }
+
+ input[type="checkbox"] {
+ margin-right: 6px;
+ accent-color: #16a34a;
+ transform: scale(1.3);
+ }
+`;
+
+export const RemoveButton = styled(BaseButton)`
+ padding: 8px 12px;
+ white-space: nowrap;
+
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #991b1b;
+
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ background: #fee2e2;
+ border-color: #fca5a5;
+ }
+ }
+
+ &:focus-visible {
+ outline: 3px solid #fecaca;
+ outline-offset: 2px;
+ border-color: #dc2626;
+ }
+`;
+
+export const Paragraf = styled.p`
+ font-size: 16px;
+ color: #1d4ed8;
+ text-align: center;
+ margin: 16px 0;
+ padding-bottom: 15px;
+`;
diff --git a/src/utils/formatDate.js b/src/utils/formatDate.js
new file mode 100644
index 00000000..c30b0d3b
--- /dev/null
+++ b/src/utils/formatDate.js
@@ -0,0 +1,8 @@
+// Format a timestamp into a readable English date string
+export function formatDate(timestamp) {
+ return new Date(timestamp).toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ });
+}
diff --git a/src/utils/todosStorage.js b/src/utils/todosStorage.js
new file mode 100644
index 00000000..65dcce66
--- /dev/null
+++ b/src/utils/todosStorage.js
@@ -0,0 +1,12 @@
+// Read saved todos from localStorage on app startup
+export function getInitialTodos() {
+ const saved = localStorage.getItem("todos");
+
+ // Parse stored JSON or return an empty array if nothing is saved
+ return saved ? JSON.parse(saved) : [];
+}
+
+// Save the current todos state to localStorage
+export function saveTodos(todos) {
+ localStorage.setItem("todos", JSON.stringify(todos));
+}