diff --git a/README.md b/README.md index d1c68b57..4daf910a 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# Todo \ No newline at end of file +A simple and responsive todo app built with React and Zustand. Using styled-components fopr styling. +The app allows users to add, complete, and remove tasks, while keeping a clean and accessible UI. +The app features: +Add new tasks +Mark tasks as completed and undo completion +Remove tasks +See how many tasks are remaining +Clear all completed tasks +Responsive design +Accessible UI following basic accessibility guidelines \ No newline at end of file diff --git a/package.json b/package.json index caf62893..b40fb595 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ }, "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", diff --git a/src/App.jsx b/src/App.jsx index 54275400..c2f7dd07 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,61 @@ -export const App = () => { +import { useTodoStore } from "./store/todoStore"; +import TaskForm from "./components/TaskForm"; +import TaskList from "./components/TaskList"; +import NoTasks from "./components/NoTasks"; +import { GlobalStyles } from "./styles/GlobalStyles"; +import { Container, Header } from "./styles/Layout"; + +export default function App() { + const tasks = useTodoStore((state) => state.tasks); + const clear = useTodoStore((state) => state.clearCompleted); + + const openCount = tasks.filter((t) => !t.completed).length; + const doneCount = tasks.filter((t) => t.completed).length; + return ( -

React Boilerplate

- ) + + +
+

On My List

+ {tasks.length > 0 &&

{openCount} Tasks Remaining

} +
+ + + + + {tasks.length === 0 ? : } + + {doneCount > 0 && ( +
+

+ Completed — {doneCount} +

+ + + + +
+ )} +
+ ); } diff --git a/src/components/NoTasks.jsx b/src/components/NoTasks.jsx new file mode 100644 index 00000000..06f65ba9 --- /dev/null +++ b/src/components/NoTasks.jsx @@ -0,0 +1,7 @@ +export default function NoTasks() { + return ( +
+

No tasks yet.

+
+ ); +} \ No newline at end of file diff --git a/src/components/Task.jsx b/src/components/Task.jsx new file mode 100644 index 00000000..81ccee9e --- /dev/null +++ b/src/components/Task.jsx @@ -0,0 +1,26 @@ +import { useTodoStore } from "../store/todoStore"; +import { ItemRow, DeleteButton } from "../styles/TaskStyles"; + +export default function Task({ task }) { + const toggle = useTodoStore((s) => s.toggleTask); + const remove = useTodoStore((s) => s.removeTask); + + return ( + + toggle(task.id)} + /> + + + + remove(task.id)}> + ✕ + + + ); +} diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx new file mode 100644 index 00000000..d22e21b3 --- /dev/null +++ b/src/components/TaskForm.jsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { useTodoStore } from "../store/todoStore"; +import { Form } from "../styles/TaskStyles"; + +export default function TaskForm() { + const addTask = useTodoStore((state) => state.addTask); + const [text, setText] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + addTask(text); + setText(""); + }; + + return ( +
+ + setText(e.target.value)} + placeholder="Add a task..." + /> + +
+ ); +} \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx new file mode 100644 index 00000000..14c535aa --- /dev/null +++ b/src/components/TaskList.jsx @@ -0,0 +1,20 @@ +import { useTodoStore } from "../store/todoStore"; +import Task from "./Task"; +import { List } from "../styles/TaskStyles"; + +export default function TaskList({ filter }) { + const tasks = useTodoStore((state) => state.tasks); + + const filteredTasks = tasks.filter((t) => { + if (filter === "completed") return t.completed; + return !t.completed; + }); + + return ( + + {filteredTasks.map((t) => ( + + ))} + + ); +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index f7c0aef5..e69de29b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; -} diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9b..edc6cdba 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,9 @@ -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' - -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById("root")).render( -) +); \ No newline at end of file diff --git a/src/store/todoStore.js b/src/store/todoStore.js new file mode 100644 index 00000000..4ee5f11d --- /dev/null +++ b/src/store/todoStore.js @@ -0,0 +1,37 @@ +import { create } from "zustand"; + +export const useTodoStore = create((set, get) => ({ + tasks: [], + + addTask: (text) => { + if (!text.trim()) return; + + const newTask = { + id: new Date().getTime(), + title: text, + completed: false, + }; + set((state) => ({ tasks: [newTask, ...state.tasks] })); + }, + + toggleTask: (id) => { + set((state) => ({ + tasks: state.tasks.map((t) => + t.id === id ? { ...t, completed: !t.completed } : t + ), + })); + }, + + removeTask: (id) => { + set((state) => ({ + tasks: state.tasks.filter((t) => t.id !== id), + })); + }, + + clearCompleted: () => { + set((state) => ({ + tasks: state.tasks.filter((task) => !task.completed), + })); +}, + +})); \ No newline at end of file diff --git a/src/styles/GlobalStyles.js b/src/styles/GlobalStyles.js new file mode 100644 index 00000000..9cadb0cd --- /dev/null +++ b/src/styles/GlobalStyles.js @@ -0,0 +1,27 @@ +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyles = createGlobalStyle` + :root { + --app-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +} + + + html, body, #root { + margin: 0; + padding: 0; + font-family: var(--app-font); + background: #fff; + color: #000; + -webkit-font-smoothing: antialiased; + } + + /* Force form controls */ + button, input, textarea, select { + font-family: var(--app-font); + font: inherit; + } + + *, *::before, *::after { + box-sizing: border-box; + } +`; diff --git a/src/styles/Layout.js b/src/styles/Layout.js new file mode 100644 index 00000000..c6a9c42d --- /dev/null +++ b/src/styles/Layout.js @@ -0,0 +1,38 @@ +import styled from "styled-components"; + +export const Container = styled.main` + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding: 60px 20px; +`; + +export const Header = styled.header` + width: 100%; + max-width: 600px; + margin-bottom: 40px; + + h1 { + font-size: 66px; + font-weight: 800; + margin: 0; + color: #000; + letter-spacing: -2px; + } + + @media (max-width: 600px) { + h1 { + font-size: 42px; + } + } + + p { + color: #000; + font-size: 14px; + margin-top: 5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 2px; + } +`; \ No newline at end of file diff --git a/src/styles/TaskStyles.js b/src/styles/TaskStyles.js new file mode 100644 index 00000000..cd528953 --- /dev/null +++ b/src/styles/TaskStyles.js @@ -0,0 +1,101 @@ +import styled from "styled-components"; + +export const Form = styled.form` + display: flex; + width: 100%; + max-width: 600px; + gap: 12px; + margin-bottom: 40px; + + input { + font-family: var(--header-font); + flex: 1; + background: white; + border: 2px solid black; + border-radius: 12px; + padding: 16px; + font-size: 18px; + outline: none; + } + + button { + background: black; + color: white; + border: 2px solid black; + border-radius: 12px; + padding: 0 25px; + font-weight: bold; + cursor: pointer; + + &:disabled { + background: gray; + border-color: gray; + cursor: not-allowed; + } + } +`; + +export const List = styled.ul` + width: 100%; + max-width: 600px; + padding: 0; + list-style: none; +`; + +export const ItemRow = styled.li` + display: flex; + align-items: center; + background: white; + border: 2px solid black; + border-radius: 12px; + padding: 15px; + margin-bottom: 12px; + gap: 15px; + + label { + flex: 1; + font-size: 18px; + font-weight: 400; + color: black; + cursor: pointer; + } + + .checked { + color: #888; + text-decoration: line-through; + } + + input[type="checkbox"] { + width: 20px; + height: 20px; + accent-color: black; + } +`; + +export const DeleteButton = styled.button` + background: none; + border: none; + font-size: 20px; + color: black; + opacity: 0.3; + cursor: pointer; + + &:hover { + opacity: 1; + } +`; + +export const FooterButton = styled.button` + background: white; + border: 2px solid black; + padding: 10px 20px; + border-radius: 10px; + font-weight: bold; + margin-top: 20px; + cursor: pointer; + + &:hover { + background: black; + color: white; + } +`; \ No newline at end of file