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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
# Todo
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 59 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<h1>React Boilerplate</h1>
)
<Container>
<GlobalStyles />
<Header>
<h1>On My List</h1>
{tasks.length > 0 && <p>{openCount} Tasks Remaining</p>}
</Header>

<TaskForm />


{tasks.length === 0 ? <NoTasks /> : <TaskList filter="active" />}

{doneCount > 0 && (
<div style={{ width: "100%", maxWidth: "600px", marginTop: "40px" }}>
<p
style={{
color: "#444",
textTransform: "uppercase",
fontSize: "12px",
letterSpacing: "2px",
marginBottom: "15px",
}}
>
Completed — {doneCount}
</p>

<TaskList filter="completed" />

<button
onClick={clear}
style={{
background: "none",
border: "none",
color: "#444",
marginTop: "20px",
cursor: "pointer",
textDecoration: "underline",
}}
>
Clear Completed
</button>
</div>
)}
</Container>
);
}
7 changes: 7 additions & 0 deletions src/components/NoTasks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function NoTasks() {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#909091' }}>
<p>No tasks yet.</p>
</div>
);
}
26 changes: 26 additions & 0 deletions src/components/Task.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<ItemRow>
<input
type="checkbox"
id={task.id}
checked={task.completed}
onChange={() => toggle(task.id)}
/>

<label htmlFor={task.id} className={task.completed ? "checked" : ""}>
{task.title}
</label>

<DeleteButton type="button" onClick={() => remove(task.id)}>
</DeleteButton>
</ItemRow>
);
}
27 changes: 27 additions & 0 deletions src/components/TaskForm.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Form onSubmit={handleSubmit}>
<label htmlFor="task-input" style={{ display: 'none' }}>New Task</label>
<input
id="task-input"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add a task..."
/>
<button type="submit" disabled={!text.trim()}>Add</button>
</Form>
);
}
20 changes: 20 additions & 0 deletions src/components/TaskList.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<List>
{filteredTasks.map((t) => (
<Task key={t.id} task={t} />
))}
</List>
);
}
3 changes: 0 additions & 3 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
}
13 changes: 5 additions & 8 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
)
);
37 changes: 37 additions & 0 deletions src/store/todoStore.js
Original file line number Diff line number Diff line change
@@ -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),
}));
},

}));
27 changes: 27 additions & 0 deletions src/styles/GlobalStyles.js
Original file line number Diff line number Diff line change
@@ -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;
}
`;
38 changes: 38 additions & 0 deletions src/styles/Layout.js
Original file line number Diff line number Diff line change
@@ -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;
}
`;
Loading