From 7a4622c4ea86d350a92a0a7b63c6f2da7e43ed25 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 09:12:34 +0200 Subject: [PATCH 01/18] gitignore, readme, pull_req file changed --- .gitignore | 2 ++ README.md | 4 +++- package.json | 4 +++- pull_request_template.md | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b02a1ff..9564343 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ package-lock.json *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/README.md b/README.md index d1c68b5..330e628 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# Todo \ No newline at end of file +# Todo + +Todo App is a mobile-first task manager built with React, Styled Components and Zustand for global state. It lets you quickly add, toggle and remove tasks in a clean, responsive UI—no prop-drilling required. diff --git a/package.json b/package.json index caf6289..cc6d7f3 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.1.18", + "zustand": "^5.0.4" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/pull_request_template.md b/pull_request_template.md index 154c92e..332eb68 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1 @@ -Please include your Netlify link here. \ No newline at end of file +Netlify link here [zustandtodoapp.netlify.app] From 8cbaff9fe8a0e0498cc2941be3dad21de34eb8ae Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 09:26:22 +0200 Subject: [PATCH 02/18] project structure --- src/components/Header.jsx | 11 +++++++++++ src/components/TaskForm.jsx | 0 src/components/TaskItem.jsx | 0 src/components/TaskList.jsx | 0 src/store/useTaskStore.js | 0 src/styles/globalStyles.js | 0 6 files changed, 11 insertions(+) create mode 100644 src/components/Header.jsx create mode 100644 src/components/TaskForm.jsx create mode 100644 src/components/TaskItem.jsx create mode 100644 src/components/TaskList.jsx create mode 100644 src/store/useTaskStore.js create mode 100644 src/styles/globalStyles.js diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..485202a --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Header = () => ( +
+

My Zustand Todo App

+
+); + +export default Header; diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js new file mode 100644 index 0000000..e69de29 diff --git a/src/styles/globalStyles.js b/src/styles/globalStyles.js new file mode 100644 index 0000000..e69de29 From a4fa5d9f93c0bc65f9aac4d8a6e520d70f589897 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 10:44:28 +0200 Subject: [PATCH 03/18] installed uuidv4 and did actions AddTask in useTaskStore --- package.json | 1 + src/store/useTaskStore.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/package.json b/package.json index cc6d7f3..59abe3c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "styled-components": "^6.1.18", + "uuid": "^11.1.0", "zustand": "^5.0.4" }, "devDependencies": { diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js index e69de29..e68770e 100644 --- a/src/store/useTaskStore.js +++ b/src/store/useTaskStore.js @@ -0,0 +1,33 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { v4 as uuidv4 } from 'uuid'; + +const useTaskStore = create( + devtools((set) => ({ + task: [], + + AddTask: (text) => { + const trimmedText = text.trim(); + if (trimmedText === '') return; + + //add a task + const newTask = { + id: uuidv4(), + text: trimmedText, + done: false, + createdAt: new Date().toISOString(), + }; + + // mutera state + set((state) => ({ + task: [...state.task, newTask], + })); + }, + + toggleTask: (id) => {}, + removeTask: (id) => {}, + clearCompleted: () => {}, + })) +); + +export default useTaskStore; From dc2ca77140761346c9f30bcf63a39c2aef86be3c Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 11:12:35 +0200 Subject: [PATCH 04/18] toggleTask action done --- src/store/useTaskStore.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js index e68770e..50747b5 100644 --- a/src/store/useTaskStore.js +++ b/src/store/useTaskStore.js @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; const useTaskStore = create( devtools((set) => ({ - task: [], + tasks: [], AddTask: (text) => { const trimmedText = text.trim(); @@ -20,11 +20,18 @@ const useTaskStore = create( // mutera state set((state) => ({ - task: [...state.task, newTask], + tasks: [newTask, ...state.tasks], })); }, - toggleTask: (id) => {}, + toggleTask: (id) => { + set((state) => ({ + tasks: state.tasks.map((task) => { + task.id === id ? { ...task, done: !task.done } : task; + /* Om id matchar: du vill skapa en modifierad kopia av just den här tasken. Om id inte matchar någon stask: du vill låta den vara oförändrad.*/ + }), + })); + }, removeTask: (id) => {}, clearCompleted: () => {}, })) From 9de759676a9f173512e0378424c44a5fd4effec5 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 14:13:23 +0200 Subject: [PATCH 05/18] Remove and Clear done, also reviewd from ChatGPT --- src/store/useTaskStore.js | 66 ++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js index 50747b5..2b25bf1 100644 --- a/src/store/useTaskStore.js +++ b/src/store/useTaskStore.js @@ -3,38 +3,48 @@ import { devtools } from 'zustand/middleware'; import { v4 as uuidv4 } from 'uuid'; const useTaskStore = create( - devtools((set) => ({ - tasks: [], + devtools( + (set) => ({ + // STATE + tasks: [], - AddTask: (text) => { - const trimmedText = text.trim(); - if (trimmedText === '') return; + // ADD TASK + addTask: (text) => { + const trimmed = text.trim(); + if (trimmed === '') return; + const newTask = { + id: uuidv4(), + text: trimmed, + done: false, + createdAt: new Date().toISOString(), + }; + set((state) => ({ + tasks: [newTask, ...state.tasks], + })); + }, - //add a task - const newTask = { - id: uuidv4(), - text: trimmedText, - done: false, - createdAt: new Date().toISOString(), - }; + // TOGGLE TASK + toggleTask: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, done: !task.done } : task + ), + })), - // mutera state - set((state) => ({ - tasks: [newTask, ...state.tasks], - })); - }, + // REMOVE TASK + removeTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), - toggleTask: (id) => { - set((state) => ({ - tasks: state.tasks.map((task) => { - task.id === id ? { ...task, done: !task.done } : task; - /* Om id matchar: du vill skapa en modifierad kopia av just den här tasken. Om id inte matchar någon stask: du vill låta den vara oförändrad.*/ - }), - })); - }, - removeTask: (id) => {}, - clearCompleted: () => {}, - })) + // CLEAR COMPLETED TASKS + clearCompleted: () => + set((state) => ({ + tasks: state.tasks.filter((task) => task.done === false), + })), + }), + { name: 'task-store' } + ) ); export default useTaskStore; From ce9dc8538a83f836496fa6eef7a0fc282e586f50 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Wed, 21 May 2025 14:57:48 +0200 Subject: [PATCH 06/18] =?UTF-8?q?Basinneh=C3=A5ll=20i=20Header=20klar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 10 ++-- src/components/Header.jsx | 22 +++++--- src/store/useTaskStore.js | 72 ++++++++++++++------------ {src/styles => styles}/globalStyles.js | 0 4 files changed, 60 insertions(+), 44 deletions(-) rename {src/styles => styles}/globalStyles.js (100%) diff --git a/src/App.jsx b/src/App.jsx index 5427540..a22969a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,9 @@ +import Header from './components/Header'; + export const App = () => { return ( -

React Boilerplate

- ) -} + <> +
+ + ); +}; diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 485202a..cbcab64 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,11 +1,17 @@ import React from 'react'; +import useTaskStore from '../store/useTaskStore'; -const Header = () => ( -
-

My Zustand Todo App

-
-); +export default function Header() { + const totalCount = useTaskStore((state) => state.selectors.getTotalCount()); + const clearCompleted = useTaskStore((state) => state.actions.clearCompleted); -export default Header; + return ( +
+

My Zustand Todo App

+

Totalt: {totalCount} uppgift(er)

+ +
+ ); +} diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js index 2b25bf1..722307c 100644 --- a/src/store/useTaskStore.js +++ b/src/store/useTaskStore.js @@ -4,44 +4,50 @@ import { v4 as uuidv4 } from 'uuid'; const useTaskStore = create( devtools( - (set) => ({ - // STATE + (set, get) => ({ + //STATE tasks: [], - // ADD TASK - addTask: (text) => { - const trimmed = text.trim(); - if (trimmed === '') return; - const newTask = { - id: uuidv4(), - text: trimmed, - done: false, - createdAt: new Date().toISOString(), - }; - set((state) => ({ - tasks: [newTask, ...state.tasks], - })); - }, + //ACTIONS + actions: { + addTask: (text) => { + const trimmed = text.trim(); + if (trimmed === '') return; + const newTask = { + id: uuidv4(), + text: trimmed, + done: false, + createdAt: new Date().toISOString(), + }; + set((state) => ({ + tasks: [newTask, ...state.tasks], + })); + }, + + toggleTask: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, done: !task.done } : task + ), + })), - // TOGGLE TASK - toggleTask: (id) => - set((state) => ({ - tasks: state.tasks.map((task) => - task.id === id ? { ...task, done: !task.done } : task - ), - })), + removeTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), - // REMOVE TASK - removeTask: (id) => - set((state) => ({ - tasks: state.tasks.filter((task) => task.id !== id), - })), + clearCompleted: () => + set((state) => ({ + tasks: state.tasks.filter((task) => !task.done), + })), + }, - // CLEAR COMPLETED TASKS - clearCompleted: () => - set((state) => ({ - tasks: state.tasks.filter((task) => task.done === false), - })), + //SELECTORS + selectors: { + getTotalCount: () => get().tasks.length, + getPendingCount: () => get().tasks.filter((t) => !t.done).length, + getCompletedCount: () => get().tasks.filter((t) => t.done).length, + }, }), { name: 'task-store' } ) diff --git a/src/styles/globalStyles.js b/styles/globalStyles.js similarity index 100% rename from src/styles/globalStyles.js rename to styles/globalStyles.js From d92d4de6df1b891508941fb3d01155c6a1e8c574 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Thu, 22 May 2025 08:23:45 +0200 Subject: [PATCH 07/18] tested the store in the console --- src/main.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9..779e752 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,13 @@ -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 './index.css'; +import useTaskStore from './store/useTaskStore'; // ← importera din store-hook -import { App } from './App.jsx' - -import './index.css' +window.taskStore = useTaskStore; ReactDOM.createRoot(document.getElementById('root')).render( -) +); From b430e5c5497a84c464a8602427fe6c802d1a35d2 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Thu, 22 May 2025 09:17:31 +0200 Subject: [PATCH 08/18] TaskForm basic logic set --- src/components/TaskForm.jsx | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index e69de29..78c1462 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -0,0 +1,54 @@ +import useTaskStore from '../store/useTaskStore'; +import { useState } from 'react'; + +export default function TaskForm() { + const [inputText, setInputText] = useState(''); + const addTask = useTaskStore((state) => state.actions.addTask); + + const handleChange = (event) => { + setInputText = event.target.value; + }; + + const handleSubmit = (event) => { + event.preventDefault(); + + const text = inputText.trim(); + + // 1) Ingen text alls? + if (!text) { + alert('Please enter a task'); + return; + } + + // 2) För kort? + if (text.length < 3) { + alert('Task is too short (min 3 characters)'); + return; + } + + // 3) För långt? + if (text.length > 100) { + alert('Task is too long (max 100 characters)'); + return; + } + + addTask(text); + setInputText(''); + }; + + /* + User skriver → onChange → handleChange → setInputText → React-state uppdateras → input.value ändras + */ + + return ( +
+ + +
+ ); +} From 8af43116cdaac343f2d9e5f0e10fb6e0c715d799 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Thu, 22 May 2025 09:19:34 +0200 Subject: [PATCH 09/18] setInputText fix --- src/App.jsx | 2 ++ src/components/TaskForm.jsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index a22969a..2a25390 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,11 @@ import Header from './components/Header'; +import TaskForm from './components/TaskForm'; export const App = () => { return ( <>
+ ); }; diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index 78c1462..100788e 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -6,7 +6,7 @@ export default function TaskForm() { const addTask = useTaskStore((state) => state.actions.addTask); const handleChange = (event) => { - setInputText = event.target.value; + setInputText(event.target.value); }; const handleSubmit = (event) => { From 0c439aafd8b11630af5cea62e3ef9741ba56eca1 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Thu, 22 May 2025 09:57:08 +0200 Subject: [PATCH 10/18] TaskItem logic set --- src/components/Header.jsx | 10 ++++++---- src/components/TaskItem.jsx | 34 ++++++++++++++++++++++++++++++++++ src/store/useTaskStore.js | 2 +- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/components/Header.jsx b/src/components/Header.jsx index cbcab64..aa0bd78 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -4,13 +4,15 @@ import useTaskStore from '../store/useTaskStore'; export default function Header() { const totalCount = useTaskStore((state) => state.selectors.getTotalCount()); const clearCompleted = useTaskStore((state) => state.actions.clearCompleted); + const notCompleted = useTaskStore((state) => + state.selectors.getRemainingCount() + ); return ( -
+

My Zustand Todo App

-

Totalt: {totalCount} uppgift(er)

+

Totalt: {totalCount} uppgifter

+

Remaining: {notCompleted} uppgiofter

); diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx index e69de29..df29e07 100644 --- a/src/components/TaskItem.jsx +++ b/src/components/TaskItem.jsx @@ -0,0 +1,34 @@ +import useTaskStore from '../store/useTaskStore'; + +export default function TaskItem({ task }) { + const toggleTask = useTaskStore((state) => state.actions.toggleTask); + const removeTask = useTaskStore((state) => state.actions.removeTask); + + const handleToggle = () => { + toggleTask(task.id); + }; + + const handleRemove = () => { + removeTask(task.id); + }; + + return ( +
  • +

    {task.text}

    + + +
  • + ); +} diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js index 722307c..276c998 100644 --- a/src/store/useTaskStore.js +++ b/src/store/useTaskStore.js @@ -45,7 +45,7 @@ const useTaskStore = create( //SELECTORS selectors: { getTotalCount: () => get().tasks.length, - getPendingCount: () => get().tasks.filter((t) => !t.done).length, + getRemainingCount: () => get().tasks.filter((t) => !t.done).length, getCompletedCount: () => get().tasks.filter((t) => t.done).length, }, }), From a7fcd10026bdd4aca63969803940153a11ca7910 Mon Sep 17 00:00:00 2001 From: HolaCarmensita Date: Thu, 22 May 2025 10:14:35 +0200 Subject: [PATCH 11/18] ListItem and TaskList implemeted togehter --- src/App.jsx | 2 ++ src/components/EmtyState.jsx | 12 ++++++++++++ src/components/TaskItem.jsx | 5 +++-- src/components/TaskList.jsx | 23 +++++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/components/EmtyState.jsx diff --git a/src/App.jsx b/src/App.jsx index 2a25390..9f94e26 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,13 @@ import Header from './components/Header'; import TaskForm from './components/TaskForm'; +import TaskList from './components/TaskList'; export const App = () => { return ( <>
    + ); }; diff --git a/src/components/EmtyState.jsx b/src/components/EmtyState.jsx new file mode 100644 index 0000000..e3ddb7c --- /dev/null +++ b/src/components/EmtyState.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import styled from 'styled-components'; + +const Message = styled.p` + text-align: center; + color: #666; + margin-top: 2rem; +`; + +export default function EmptyState({ text }) { + return {text}; +} diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx index df29e07..fd122d2 100644 --- a/src/components/TaskItem.jsx +++ b/src/components/TaskItem.jsx @@ -14,13 +14,14 @@ export default function TaskItem({ task }) { return (
  • -

    {task.text}

    -