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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# Todo
# Todo

This app should be able to:

- Add a task
- See all the tasks
- Mark the tasks that are ready/not ready
- Remove a task
- See how many tasks there are in total
- Every file has its own responsibility for readability. Even Empty State has its own file.

The app should be simle and easy to use. A header, an inputfield with an add-button. Then the todos comes up underneath. Maybe also showing date and time and when the todo was posted. I saw a nice color-combination on Pinterest and use that in my app.

I am using Zustand since its a requiery for this course.

Full Accessibility will be applied.

For styling I use Styled-components.

Link to Netlify: https://todo-app-ml.netlify.app/
46 changes: 32 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />

<!-- Google font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">

<!-- Ensure Roboto and background loads immediately -->
<style>
body {
background-color: #F0E7d5;
color: #212842;
font-family: 'Roboto', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="./src/main.jsx">
</script>
</body>

</html>
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.6",
"zustand": "^5.0.10"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
10 changes: 7 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { TodoInput } from "./components/TodoInput";

export const App = () => {
return (
<h1>React Boilerplate</h1>
)
}
<>
<TodoInput />
</>
);
};
164 changes: 164 additions & 0 deletions src/components/TodoInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { useState } from "react";
import styled from "styled-components";
import { colors } from "../styles/colors";
import { useTodoStore } from "../stores/todoStore";
import { TodoList } from "./TodoList";

export const TodoInput = () => {
const addTodo = useTodoStore((s) => s.addTodo);
const totalCount = useTodoStore((s) => s.todos.length);
const completedCount = useTodoStore((s) => s.todos.filter((t) => t.completed).length);
const remainingCount = useTodoStore((s) => s.todos.filter((t) => !t.completed).length);
const [todoText, setTodoText] = useState("")

const handleAddTodo = () => {
const trimmed = todoText.trim();
if (!trimmed) return;
addTodo(trimmed);
setTodoText("");
};

return (
<>
<HeaderContainer>
<Header>ToDo</Header>
</HeaderContainer>

<Container>
<Main>
<CountRow>
<Count type="remaining">{remainingCount} Not ready</Count>
<Count type="completed">{completedCount} Completed</Count>
<Count type="total">{totalCount} Total</Count>
</CountRow>

<InputWrapper>
<label htmlFor="new-todo" className="sr-only">
</label>
<Input
id="new-todo"
type="text"
value={todoText}
onKeyDown={(e) => e.key === "Enter" && handleAddTodo()}
onChange={(e) => setTodoText(e.target.value)}
placeholder="Add your todo"
/>

<Button type="button" onClick={handleAddTodo}>
Add
</Button>
</InputWrapper>

<TodoList />
</Main>
</Container>
</>
);
};

const Container = styled.section`
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
padding: 16px;

@media (min-width: 768px) {
padding 24px;
}
`;

const HeaderContainer = styled.header`
display: flex;
justify-content: center;
background: ${colors.primary};
width: 100%;
align-items: center;
padding: 16px 0;

@media (min-width: 768px) {
padding: 20px 0;
}
`;

const Header = styled.h1`
font-size: 48px;
color: ${colors.secondary};
`;

const Main = styled.main`
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 32px;
align-items: center;
`;

const CountRow = styled.div`
display: flex;
justify-content: center;
gap: 8px;
margin-top: 8px;

@media (min-width: 768px) {
gap: 16px;
}
`;

const Count = styled.div`
background: ${colors.primary};
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
color: ${colors.secondary};
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;

const Button = styled.button`
width: 70px;
height: 40px;
font-size: 16px;
background: none;
border: 1px solid #ccc;
border-radius: 8px;
color: ${colors.secondary};
font-family: inherit;

@media (min-width: 480px) {
width: 60px;
height: 36px;
font-size: 14px;
}
`;

const InputWrapper = styled.section`
display: flex;
width: 100%;
max-width: 800px;
align-items: center;
justify-content: center;
padding: 20px;
background: ${colors.primary};
gap: 12px;

@media (min-width: 768px) {
gap: 16px;
padding: 20px;
}
`;

const Input = styled.input`
flex: 1;
min-width: 0;
padding: 10px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 8px;
font-family: inherit;

@media (min-width: 768px) {
font-size: 18px;
padding: 10px 15px;
}
`;
76 changes: 76 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";
import { useTodoStore } from "../stores/todoStore";
import styled from "styled-components";
import { colors } from "../styles/colors";


export const TodoItem = ({ todo }) => {
const removeTodo = useTodoStore((state) => state.removeTodo);
const toggleTodo = useTodoStore((state) => state.toggleTodo);

const textId = `todo-text-${todo.id}`;

return (
<Item role="listitem" color={todo.color}>
<Checkbox
type="checkbox"
checked={todo.completed}
aria-labelledby={`todo-text-${todo.id}`}
onChange={() => toggleTodo(todo.id)}
/>
<Text
id={textId}
$completed={todo.completed}
$hasColor={!!todo.color}>{todo.text}
</Text>
<DeleteBtn
aria-label={`Remove: ${todo.text}`}
$hasColor={!!todo.color}
type="button"
onClick={() => removeTodo(todo.id)}>
🗑
</DeleteBtn>
</Item>
);
};

const Item = styled.div`
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: ${colors.primary};
font-family: inherit;
margin: 10px 0;
`;

const Checkbox = styled.input`
width: 18px;
height: 18px;
cursor: pointer;
flex: 0 0 auto;
`;

const Text = styled.span`
flex: 1;
font-size: 18px;
font-weight: 400;
color: ${colors.secondary};
text-decoration: ${(p) => (p.$completed ? "line-through" : "none")};
word-break: break-word;
font-family: inherit;
`;

const DeleteBtn = styled.button`
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: ${colors.secondary};
padding: 4px;

&:focus-visible {
outline: 2px solid ${colors.primary};
outline-offset: 2px;
}
`;
36 changes: 36 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import { useTodoStore } from "../stores/todoStore";
import { TodoItem } from "./TodoItem";
import styled from "styled-components";
import { colors } from "../styles/colors";

export const TodoList = () => {
const todos = useTodoStore((state) => state.todos);

if (todos.length === 0) return <Empty>“All quiet here… add your first task!📝”</Empty>

return (
<ListContainer role="list">
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ListContainer>
)
};

const ListContainer = styled.section`
width: 100%;
max-width: 800px;
margin: 20px auto;
background: ${colors.secondary};
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
overflow: hidden;
`;

const Empty = styled.p`
text-align: center;
color: ${colors.primary};
font-size: 18px;
`;

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;
}
Loading