diff --git a/README.md b/README.md index d1c68b57..ffa30405 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Todo \ No newline at end of file +# 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/ \ No newline at end of file diff --git a/index.html b/index.html index f7ac4e42..443f0b3b 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,34 @@ - - - - - Todo - - -
- - - + + + + + + + + + + + + + + + Todo + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index caf62893..62a5a956 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.6", + "zustand": "^5.0.10" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/App.jsx b/src/App.jsx index 54275400..de1e695e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,9 @@ +import { TodoInput } from "./components/TodoInput"; + export const App = () => { return ( -

React Boilerplate

- ) -} + <> + + + ); +}; diff --git a/src/components/TodoInput.jsx b/src/components/TodoInput.jsx new file mode 100644 index 00000000..13405651 --- /dev/null +++ b/src/components/TodoInput.jsx @@ -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 ( + <> + +
ToDo
+
+ + +
+ + {remainingCount} Not ready + {completedCount} Completed + {totalCount} Total + + + + + e.key === "Enter" && handleAddTodo()} + onChange={(e) => setTodoText(e.target.value)} + placeholder="Add your todo" + /> + + + + + +
+
+ + ); +}; + +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; + } +`; \ No newline at end of file diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 00000000..93c39ef5 --- /dev/null +++ b/src/components/TodoItem.jsx @@ -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 ( + + toggleTodo(todo.id)} + /> + {todo.text} + + removeTodo(todo.id)}> + 🗑 + + + ); +}; + +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; + } +`; \ No newline at end of file diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 00000000..35a0fa96 --- /dev/null +++ b/src/components/TodoList.jsx @@ -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 “All quiet here… add your first task!📝” + + return ( + + {todos.map((todo) => ( + + ))} + + ) +}; + +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; +`; + 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..e16dbf04 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 { GlobalStyle } from "./styles/GlobalStyle.js"; +import { App } from "./App.jsx"; -import { App } from './App.jsx' - -import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( + ) diff --git a/src/stores/todoStore.js b/src/stores/todoStore.js new file mode 100644 index 00000000..d892e367 --- /dev/null +++ b/src/stores/todoStore.js @@ -0,0 +1,35 @@ +import { create } from "zustand"; +import { colors } from "../styles/colors"; + +const twoColors = [colors.primary, colors.secondary]; + +export const useTodoStore = create((set, get) => ({ + todos: [], + + addTodo: (text) => { + + const state = get(); + + const newTodo = { + id: Date.now(), + text, + completed: false, + }; + + set({ + todos: [...state.todos, newTodo], + }); + }, + + removeTodo: (id) => + set((state) => ({ + todos: state.todos.filter(todo => todo.id !== id) + })), + + toggleTodo: (id) => + set((state) => ({ + todos: state.todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + ), + })), +})); diff --git a/src/styles/GlobalStyle.js b/src/styles/GlobalStyle.js new file mode 100644 index 00000000..e2f55197 --- /dev/null +++ b/src/styles/GlobalStyle.js @@ -0,0 +1,33 @@ +import { createGlobalStyle } from "styled-components"; +import { colors } from "./colors"; + +export const GlobalStyle = createGlobalStyle` + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + width: 100%; + height: 100%; +} + +#root { + width: 100%; +} + +body { + background-color: ${colors.secondary}; + color: ${colors.primary}; + font-family: 'Roboto', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + min-height: 100vh; + line-height: 1.5; +} + +button, input, textarea, select { + font-family: inherit; + cursor: pointer; + } +`; \ No newline at end of file diff --git a/src/styles/colors.js b/src/styles/colors.js new file mode 100644 index 00000000..68a9b4c7 --- /dev/null +++ b/src/styles/colors.js @@ -0,0 +1,4 @@ +export const colors = { + primary: "#212842", + secondary: "#F0E7d5", +}