From bdbd37410ff8ed5514c7680d48e1bd11201c576d Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 21 May 2025 21:39:08 +0200 Subject: [PATCH 01/15] started, added zustand etc. --- index.html | 2 +- package.json | 7 +++- src/App.css | 0 src/App.jsx | 10 +++++- src/components/Footer.jsx | 0 src/components/Header.jsx | 71 +++++++++++++++++++++++++++++++++++++ src/components/TaskForm.jsx | 35 ++++++++++++++++++ src/components/TaskList.jsx | 26 ++++++++++++++ src/index.css | 3 -- src/main.jsx | 2 -- src/stores/useTaskStore.js | 16 +++++++++ 11 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 src/App.css create mode 100644 src/components/Footer.jsx create mode 100644 src/components/Header.jsx create mode 100644 src/components/TaskForm.jsx create mode 100644 src/components/TaskList.jsx delete mode 100644 src/index.css create mode 100644 src/stores/useTaskStore.js diff --git a/index.html b/index.html index f7ac4e4..a14f979 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Todo + Make it happen!
diff --git a/package.json b/package.json index caf6289..3ac57e5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.6.0", + "styled-components": "^6.1.18", + "zustand": "^5.0.4" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -19,9 +22,11 @@ "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", + "eslint-config-prettier": "^10.1.5", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "prettier": "^3.5.3", "vite": "^6.2.0" } } diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..e69de29 diff --git a/src/App.jsx b/src/App.jsx index 5427540..fcb962d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,13 @@ +import { TaskForm } from "./components/TaskForm"; +import { HeaderComponent } from "./components/Header"; + +import "./App.css" + export const App = () => { return ( -

React Boilerplate

+ <> + + + ) } diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..6477618 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,71 @@ +import styled from "styled-components"; + +export const Header = styled.header` + background-color:rgb(255, 255, 255); + padding: 20px; + color: white; + + text-align: center; + font-size: 1.3rem; + font-weight: bold; + + @media (max-width: 600px) { + font-size: 1.5rem; + } + @media (max-width: 400px) { + font-size: 1.2rem; + } +` + +export const HeaderText = styled.h1` + font-size: 2.5em; + color:rgb(24, 148, 183); + margin: 0; + text-align: center; + + @media (max-width: 600px) { + font-size: 2em; + } + + @media (max-width: 400px) { + font-size: 1.5em; + } +` +export const HeaderSubtitle = styled.h2` + font-size: 1.5em; + color:rgb(0, 0, 0); + margin: 0; + text-align: center; + + @media (max-width: 600px) { + font-size: 1.2em; + } + + @media (max-width: 400px) { + font-size: 1em; + } +` +export const HeaderLogo = styled.img` + width: 100px; + height: auto; + margin: 0 auto; + display: block; + + @media (max-width: 600px) { + width: 80px; + } + + @media (max-width: 400px) { + width: 60px; + } +`; + +export const HeaderComponent = () => { + return ( +
+ + Make it happen + A ToDo-app created by Cathi +
+ ); +} \ No newline at end of file diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx new file mode 100644 index 0000000..a49a064 --- /dev/null +++ b/src/components/TaskForm.jsx @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { useTaskStore } from "../stores/useTaskStore"; +import { TaskList } from "./TaskList"; + +export const TaskForm = () => { + const [taskValue, setTaskValue] = useState(""); + const addTask = useTaskStore((state) => state.addTask); + + const handleSubmit = (e) => { + e.preventDefault(); + + if (taskValue.trim()) { + addTask(taskValue); + setTaskValue(""); + } + }; + + return ( + <> +
+ + setTaskValue(e.target.value)} + /> + +
+ + + + ); +}; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx new file mode 100644 index 0000000..7cf0593 --- /dev/null +++ b/src/components/TaskList.jsx @@ -0,0 +1,26 @@ +import { useTaskStore } from "../stores/useTaskStore"; + +export const TaskList = () => { + const tasks = useTaskStore((state) => state.tasks); + const deleteTask = useTaskStore((state) => state.deleteTask); + const completeTask = useTaskStore((state) => state.completeTask); + +return ( +
    + {tasks.map((task) => ( +
  • + + {task.text}{" "} + {!task.isCompleted ? ( + + ) : null} + +
  • + ))} +
+); +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css deleted file mode 100644 index f7c0aef..0000000 --- a/src/index.css +++ /dev/null @@ -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 1b8ffe9..b92361c 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,8 +3,6 @@ import ReactDOM from 'react-dom/client' import { App } from './App.jsx' -import './index.css' - ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/src/stores/useTaskStore.js b/src/stores/useTaskStore.js new file mode 100644 index 0000000..7649029 --- /dev/null +++ b/src/stores/useTaskStore.js @@ -0,0 +1,16 @@ +import { create } from "zustand"; + +export const useTaskStore = create((set) => ({ + tasks: [], + addTask: (text) => set((state) => ({ + tasks: [...state.tasks, { id: Date.now(), text, isCompleted: false }], + })), + deleteTask: (id) => set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), + completeTask: (id) => set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, isCompleted: !task.isCompleted } : task + ), + })), +})); \ No newline at end of file From 97c904b2c6f023c982bbcbdd93c895d0543e0831 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 21 May 2025 22:11:08 +0200 Subject: [PATCH 02/15] started with some styling --- package.json | 1 + src/components/Header.jsx | 15 ------- src/components/TaskForm.jsx | 29 +++++++++---- src/components/TaskForm.styles.jsx | 53 ++++++++++++++++++++++++ src/components/TaskList.jsx | 50 ++++++++++++++--------- src/components/TaskList.styles.jsx | 65 ++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 41 deletions(-) create mode 100644 src/components/TaskForm.styles.jsx create mode 100644 src/components/TaskList.styles.jsx diff --git a/package.json b/package.json index 3ac57e5..4e07c1f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.6.0", "styled-components": "^6.1.18", "zustand": "^5.0.4" diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 6477618..54db723 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -44,26 +44,11 @@ export const HeaderSubtitle = styled.h2` @media (max-width: 400px) { font-size: 1em; } -` -export const HeaderLogo = styled.img` - width: 100px; - height: auto; - margin: 0 auto; - display: block; - - @media (max-width: 600px) { - width: 80px; - } - - @media (max-width: 400px) { - width: 60px; - } `; export const HeaderComponent = () => { return (
- Make it happen A ToDo-app created by Cathi
diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index a49a064..bd8dd33 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -1,6 +1,16 @@ import { useState } from "react"; import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; +import { SlPlus } from "react-icons/sl"; +import { + FormContainer, + StyledForm, + FormLabel, + FormInput, + AddButton, + IconWrapper +} from './TaskForm.styles.jsx'; + export const TaskForm = () => { const [taskValue, setTaskValue] = useState(""); @@ -16,20 +26,23 @@ export const TaskForm = () => { }; return ( - <> -
- - + + + setTaskValue(e.target.value)} + placeholder="Enter a new task" + required /> - - - + + + +
- + ); }; \ No newline at end of file diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx new file mode 100644 index 0000000..6ebdbc3 --- /dev/null +++ b/src/components/TaskForm.styles.jsx @@ -0,0 +1,53 @@ +import styled from 'styled-components'; + +export const FormContainer = styled.div` + max-width: 600px; + padding: 20px; + background-color: #e0f2f7; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + margin: 0 auto; + box-sizing: border-box; +`; + +export const StyledForm = styled.form` + display: flex; + gap: 10px; + align-items: center; +`; + +export const FormLabel = styled.label` + font-weight: bold; + color: #333; +`; + +export const FormInput = styled.input` + flex-grow: 1; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1em; +`; + +export const AddButton = styled.button` +background: none; + color: black; + padding: 5px 5px; + border: none; + cursor: pointer; + font-size: 22px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: #007bb5; + } +`; + +export const IconWrapper = styled.span` + margin-right: 5px; /* Mellanrum mellan ikon och text/symbol */ + font-size: 1.2em; + line-height: 1; /* För att bättre kontrollera ikonens vertikala justering */ +`; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 7cf0593..e80a19e 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,26 +1,40 @@ import { useTaskStore } from "../stores/useTaskStore"; +import { SlCheck, SlClose } from "react-icons/sl"; +import { + StyledTaskList, + TaskItem, + TaskText, + CompleteButton, + DeleteButton, + TaskActions, + Icon +} from './TaskList.styles.jsx'; export const TaskList = () => { const tasks = useTaskStore((state) => state.tasks); const deleteTask = useTaskStore((state) => state.deleteTask); const completeTask = useTaskStore((state) => state.completeTask); -return ( -
    - {tasks.map((task) => ( -
  • - - {task.text}{" "} - {!task.isCompleted ? ( - - ) : null} - -
  • - ))} -
-); + return ( + + {tasks.map((task) => ( + + {task.text} + + + {!task.isCompleted ? ( + completeTask(task.id)}> + + + ) : null} + + deleteTask(task.id)}> + + + + + + ))} + + ); }; \ No newline at end of file diff --git a/src/components/TaskList.styles.jsx b/src/components/TaskList.styles.jsx new file mode 100644 index 0000000..724fce3 --- /dev/null +++ b/src/components/TaskList.styles.jsx @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +export const StyledTaskList = styled.ul` + list-style: none; + padding: 0; + margin-top: 20px; +`; + +export const TaskItem = styled.li` +font-size: 18px; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #f9f9f9; + border: 1px solid #eee; + padding: 10px 15px; + margin-bottom: 8px; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + // Dynamic styling based on props + text-decoration: ${(props) => (props.$isCompleted ? 'line-through' : 'unset')}; + color: ${(props) => (props.$isCompleted ? '#888' : 'inherit')}; +`; + +export const TaskText = styled.span` + flex-grow: 1; +`; + +export const TaskActionButton = styled.button` +font-size: 22px; + margin-left: 10px; + padding: 5px 10px; + border: none; + border-radius: 3px; + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + opacity: 0.8; + } +`; + +export const CompleteButton = styled(TaskActionButton)` + background-color: #4CAF50; /* Grön */ + color: white; +`; + +export const DeleteButton = styled(TaskActionButton)` + background-color: #f44336; /* Röd */ + color: white; +`; + +export const TaskActions = styled.div` + display: flex; + gap: 5px; /* Mellanrum mellan knapparna */ +`; + +export const Icon = styled.span` + margin-right: 5px; /* Mellanrum mellan ikon och text om det finns någon */ + font-size: 1.2em; +`; \ No newline at end of file From ba457d99bdf88138b7108e7789adf1b99043f358 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 22 May 2025 12:07:46 +0200 Subject: [PATCH 03/15] remove all task function and button --- src/components/Count.jsx | 10 +++++ src/components/Header.jsx | 4 +- src/components/TaskForm.jsx | 19 +++++++-- src/components/TaskForm.styles.jsx | 63 ++++++++++++++++++++++++------ src/components/TaskList.jsx | 31 +++++++++------ src/components/TaskList.styles.jsx | 56 +++++++++++++++++--------- src/stores/useTaskStore.js | 30 ++++++++------ 7 files changed, 155 insertions(+), 58 deletions(-) create mode 100644 src/components/Count.jsx diff --git a/src/components/Count.jsx b/src/components/Count.jsx new file mode 100644 index 0000000..387da24 --- /dev/null +++ b/src/components/Count.jsx @@ -0,0 +1,10 @@ +import useTaskStore from "../stores/useTaskStore"; + +const Counter = () => { + const tasks = useTaskStore((state) => state.tasks); + const notCompleted = tasks.filter((task) => !task.completed).length; + + return

Uncompleted tasks: {notCompleted}

; +} + +export default Counter; \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 54db723..1321685 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -32,9 +32,9 @@ export const HeaderText = styled.h1` } ` export const HeaderSubtitle = styled.h2` - font-size: 1.5em; + font-size: 1em; color:rgb(0, 0, 0); - margin: 0; + margin-top: 10px; text-align: center; @media (max-width: 600px) { diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index bd8dd33..11b46ba 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -2,19 +2,22 @@ import { useState } from "react"; import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; import { SlPlus } from "react-icons/sl"; +import { FaRegTrashAlt } from "react-icons/fa"; import { FormContainer, StyledForm, FormLabel, FormInput, AddButton, - IconWrapper + IconWrapper, + ClearAllButton, } from './TaskForm.styles.jsx'; export const TaskForm = () => { const [taskValue, setTaskValue] = useState(""); const addTask = useTaskStore((state) => state.addTask); + const removeAllTasks = useTaskStore((state) => state.removeAllTasks); const handleSubmit = (e) => { e.preventDefault(); @@ -25,6 +28,12 @@ export const TaskForm = () => { } }; + const handleClearAll = () => { + if (window.confirm("Are you sure you want to delete all tasks? This action cannot be undone.")) { + removeAllTasks(); + } + }; + return ( @@ -35,14 +44,18 @@ export const TaskForm = () => { name="newTask" value={taskValue} onChange={(e) => setTaskValue(e.target.value)} - placeholder="Enter a new task" + placeholder="Add a new task here" required /> - + + + + Delete all tasks + ); }; \ No newline at end of file diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 6ebdbc3..672c70e 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -1,15 +1,17 @@ import styled from 'styled-components'; export const FormContainer = styled.div` - max-width: 600px; padding: 20px; background-color: #e0f2f7; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - margin: 0 auto; - box-sizing: border-box; + margin-bottom: 20px; + max-width: 600px; + margin-left: auto; + margin-right: auto; `; + export const StyledForm = styled.form` display: flex; gap: 10px; @@ -19,6 +21,14 @@ export const StyledForm = styled.form` export const FormLabel = styled.label` font-weight: bold; color: #333; + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; `; export const FormInput = styled.input` @@ -27,27 +37,58 @@ export const FormInput = styled.input` border: 1px solid #ccc; border-radius: 4px; font-size: 1em; + min-height: 40px; `; + export const AddButton = styled.button` -background: none; - color: black; - padding: 5px 5px; + background-color: #008CBA; + color: white; + width: 40px; + height: 40px; border: none; + border-radius: 50%; cursor: pointer; - font-size: 22px; + font-size: 1.5em; font-weight: bold; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; &:hover { - color: #007bb5; + background-color: #007bb5; } `; export const IconWrapper = styled.span` - margin-right: 5px; /* Mellanrum mellan ikon och text/symbol */ - font-size: 1.2em; - line-height: 1; /* För att bättre kontrollera ikonens vertikala justering */ + display: flex; + align-items: center; + justify-content: center; +`; + +export const ClearAllButton = styled.button` + background-color: #dc3545; + color: white; + width: 100%; + height: 45px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1.1em; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &:hover { + background-color: #c82333; + } + + ${IconWrapper} { + margin-right: 8px; + font-size: 1.3em; + } `; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index e80a19e..0bd80eb 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,13 +1,16 @@ +import React from 'react'; import { useTaskStore } from "../stores/useTaskStore"; -import { SlCheck, SlClose } from "react-icons/sl"; +import { FaRegTrashAlt } from "react-icons/fa"; + import { StyledTaskList, TaskItem, TaskText, - CompleteButton, DeleteButton, TaskActions, - Icon + Icon, + CheckboxContainer, + TaskCheckbox } from './TaskList.styles.jsx'; export const TaskList = () => { @@ -19,19 +22,21 @@ export const TaskList = () => { {tasks.map((task) => ( - {task.text} + + completeTask(task.id)} + /> + {task.text} + - {!task.isCompleted ? ( - completeTask(task.id)}> - - - ) : null} - - deleteTask(task.id)}> - + deleteTask(task.id)} title="Delete Task"> + + - ))} diff --git a/src/components/TaskList.styles.jsx b/src/components/TaskList.styles.jsx index 724fce3..20c447a 100644 --- a/src/components/TaskList.styles.jsx +++ b/src/components/TaskList.styles.jsx @@ -4,10 +4,12 @@ export const StyledTaskList = styled.ul` list-style: none; padding: 0; margin-top: 20px; + max-width: 600px; + margin-left: auto; + margin-right: auto; `; export const TaskItem = styled.li` -font-size: 18px; display: flex; align-items: center; justify-content: space-between; @@ -17,49 +19,67 @@ font-size: 18px; margin-bottom: 8px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + min-height: 40px; + box-sizing: border-box; +`; - // Dynamic styling based on props - text-decoration: ${(props) => (props.$isCompleted ? 'line-through' : 'unset')}; - color: ${(props) => (props.$isCompleted ? '#888' : 'inherit')}; +export const CheckboxContainer = styled.div` + display: flex; + align-items: center; + flex-grow: 1; + cursor: pointer; + padding-right: 10px; +`; + +export const TaskCheckbox = styled.input` + width: 20px; + height: 20px; + margin-right: 10px; + cursor: pointer; + flex-shrink: 0; `; export const TaskText = styled.span` - flex-grow: 1; + text-decoration: ${(props) => (props.$isCompleted ? 'line-through' : 'unset')}; + color: ${(props) => (props.$isCompleted ? '#888' : 'inherit')}; + word-break: break-word; + font-size: 1em; + line-height: 1.4; `; export const TaskActionButton = styled.button` -font-size: 22px; - margin-left: 10px; - padding: 5px 10px; + padding: 8px; + width: 40px; + height: 40px; border: none; - border-radius: 3px; + border-radius: 50%; cursor: pointer; font-weight: bold; display: flex; align-items: center; justify-content: center; + font-size: 1.2em; + flex-shrink: 0; &:hover { opacity: 0.8; } `; -export const CompleteButton = styled(TaskActionButton)` - background-color: #4CAF50; /* Grön */ - color: white; -`; - export const DeleteButton = styled(TaskActionButton)` - background-color: #f44336; /* Röd */ + background-color: #f44336; color: white; `; export const TaskActions = styled.div` display: flex; - gap: 5px; /* Mellanrum mellan knapparna */ + gap: 5px; + flex-shrink: 0; + align-items: center; `; export const Icon = styled.span` - margin-right: 5px; /* Mellanrum mellan ikon och text om det finns någon */ - font-size: 1.2em; + display: flex; + align-items: center; + justify-content: center; `; \ No newline at end of file diff --git a/src/stores/useTaskStore.js b/src/stores/useTaskStore.js index 7649029..3c5fbed 100644 --- a/src/stores/useTaskStore.js +++ b/src/stores/useTaskStore.js @@ -2,15 +2,23 @@ import { create } from "zustand"; export const useTaskStore = create((set) => ({ tasks: [], - addTask: (text) => set((state) => ({ - tasks: [...state.tasks, { id: Date.now(), text, isCompleted: false }], - })), - deleteTask: (id) => set((state) => ({ - tasks: state.tasks.filter((task) => task.id !== id), - })), - completeTask: (id) => set((state) => ({ - tasks: state.tasks.map((task) => - task.id === id ? { ...task, isCompleted: !task.isCompleted } : task - ), - })), + + addTask: (text) => + set((state) => ({ + tasks: [...state.tasks, { id: Date.now(), text, isCompleted: false }], + })), + + deleteTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), + + completeTask: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, isCompleted: !task.isCompleted } : task + ), + + removeAllTasks: () => set({ tasks: [] }), + })), })); \ No newline at end of file From 9cd93f5913263818619e87fa96636e60d81cf64d Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 22 May 2025 12:56:12 +0200 Subject: [PATCH 04/15] updated todo --- src/components/TaskForm.jsx | 21 ++++++++++++++++----- src/components/TaskForm.styles.jsx | 30 ++++++++++++++++++++++++++++-- src/stores/useTaskStore.js | 11 ++++++++--- todo.txt | 8 ++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 todo.txt diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index 11b46ba..520b4ea 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -3,6 +3,7 @@ import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; import { SlPlus } from "react-icons/sl"; import { FaRegTrashAlt } from "react-icons/fa"; +import { MdFileDownloadDone } from "react-icons/md"; import { FormContainer, StyledForm, @@ -10,13 +11,15 @@ import { FormInput, AddButton, IconWrapper, - ClearAllButton, + DeleteAllButton, + CompleteAllButton } from './TaskForm.styles.jsx'; export const TaskForm = () => { const [taskValue, setTaskValue] = useState(""); const addTask = useTaskStore((state) => state.addTask); + const completeAllTasks = useTaskStore((state) => state.completeAllTasks); const removeAllTasks = useTaskStore((state) => state.removeAllTasks); const handleSubmit = (e) => { @@ -28,7 +31,11 @@ export const TaskForm = () => { } }; - const handleClearAll = () => { + const handleCompleteAll = () => { + completeAllTasks(); + }; + + const handleDeleteAll = () => { if (window.confirm("Are you sure you want to delete all tasks? This action cannot be undone.")) { removeAllTasks(); } @@ -52,10 +59,14 @@ export const TaskForm = () => { - - + + + Complete all tasks + + + Delete all tasks - + ); }; \ No newline at end of file diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 672c70e..5a24c9a 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -67,10 +67,10 @@ export const IconWrapper = styled.span` justify-content: center; `; -export const ClearAllButton = styled.button` +export const DeleteAllButton = styled.button` background-color: #dc3545; color: white; - width: 100%; + width: 40%; height: 45px; border: none; border-radius: 4px; @@ -87,6 +87,32 @@ export const ClearAllButton = styled.button` background-color: #c82333; } + ${IconWrapper} { + margin-right: 8px; + font-size: 1.3em; + } +`; + +export const CompleteAllButton = styled.button` + background-color: rgb(19, 152, 19); + color: white; + width: 40%; + height: 45px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1.1em; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + &:hover { + background-color: rgba(19, 152, 19, 0.57); + } + ${IconWrapper} { margin-right: 8px; font-size: 1.3em; diff --git a/src/stores/useTaskStore.js b/src/stores/useTaskStore.js index 3c5fbed..90f5b08 100644 --- a/src/stores/useTaskStore.js +++ b/src/stores/useTaskStore.js @@ -7,18 +7,23 @@ export const useTaskStore = create((set) => ({ set((state) => ({ tasks: [...state.tasks, { id: Date.now(), text, isCompleted: false }], })), - + deleteTask: (id) => set((state) => ({ tasks: state.tasks.filter((task) => task.id !== id), })), - + completeTask: (id) => set((state) => ({ tasks: state.tasks.map((task) => task.id === id ? { ...task, isCompleted: !task.isCompleted } : task ), - + + completeAllTasks: () => + set((state) => ({ + tasks: state.tasks.map((task) => ({ ...task, isCompleted: true })), + })), + removeAllTasks: () => set({ tasks: [] }), })), })); \ No newline at end of file diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..b917354 --- /dev/null +++ b/todo.txt @@ -0,0 +1,8 @@ +Todo: Todo + +[] implement timestamp +[] get delete and complete button to work with function +[] only show delete/complete when atleast 1 task in liste +[] add counter tho show how many task remaining/is +[] archived tab? +[] styling? From c7130fbfbf8ce2f87927ba381156209aae887373 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 22 May 2025 22:17:06 +0200 Subject: [PATCH 05/15] functions to star tasks --- src/components/TaskForm.jsx | 64 +++++++++++++----- src/components/TaskForm.styles.jsx | 61 +++++++++--------- src/components/TaskList.jsx | 69 ++++++++++++-------- src/components/TaskList.styles.jsx | 100 ++++++++++++++--------------- src/stores/useTaskStore.js | 31 +++++++-- 5 files changed, 196 insertions(+), 129 deletions(-) diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index 520b4ea..23faf92 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -1,9 +1,9 @@ + import { useState } from "react"; import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; import { SlPlus } from "react-icons/sl"; -import { FaRegTrashAlt } from "react-icons/fa"; -import { MdFileDownloadDone } from "react-icons/md"; +import { FaRegTrashAlt, FaCheckCircle, FaStar, FaRegStar } from 'react-icons/fa'; import { FormContainer, StyledForm, @@ -11,10 +11,10 @@ import { FormInput, AddButton, IconWrapper, - DeleteAllButton, - CompleteAllButton -} from './TaskForm.styles.jsx'; - + StyledFunctionButton, + ButtonRow, + ToggleListButton +} from './TaskForm.styles.jsx'; export const TaskForm = () => { const [taskValue, setTaskValue] = useState(""); @@ -22,9 +22,12 @@ export const TaskForm = () => { const completeAllTasks = useTaskStore((state) => state.completeAllTasks); const removeAllTasks = useTaskStore((state) => state.removeAllTasks); + const tasks = useTaskStore((state) => state.tasks); + const showStarredOnly = useTaskStore((state) => state.showStarredOnly); + const toggleShowStarredOnly = useTaskStore((state) => state.toggleShowStarredOnly); + const handleSubmit = (e) => { e.preventDefault(); - if (taskValue.trim()) { addTask(taskValue); setTaskValue(""); @@ -41,32 +44,63 @@ export const TaskForm = () => { } }; + const handleToggleList = () => { + toggleShowStarredOnly(); + }; + + const hasIncompleteTasks = tasks.some(task => !task.isCompleted); + const hasAnyTasks = tasks.length > 0; + return ( - + Task setTaskValue(e.target.value)} - placeholder="Add a new task here" + placeholder="Add task..." required /> + - - Complete all tasks - + {hasAnyTasks && ( + + {/* Button for toggling between all and starred tasks */} + + {showStarredOnly ? ( + <> Show all + ) : ( + <> Show starred + )} + + + {/* "Mark everyone as complete" - only shown if there are incomplete tasks */} + {hasIncompleteTasks && ( + + Mark all as complete + + )} + + {tasks.length > 0 && ( // Show only if there are tasks + + Delete All + + )} - - Delete all tasks - + + )} ); }; \ No newline at end of file diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 5a24c9a..6ce51bd 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -1,3 +1,4 @@ + import styled from 'styled-components'; export const FormContainer = styled.div` @@ -9,18 +10,18 @@ export const FormContainer = styled.div` max-width: 600px; margin-left: auto; margin-right: auto; + box-sizing: border-box; `; - export const StyledForm = styled.form` display: flex; + flex-direction: row; gap: 10px; align-items: center; + margin-bottom: 20px; `; export const FormLabel = styled.label` - font-weight: bold; - color: #333; position: absolute; width: 1px; height: 1px; @@ -37,10 +38,10 @@ export const FormInput = styled.input` border: 1px solid #ccc; border-radius: 4px; font-size: 1em; - min-height: 40px; + height: 40px; + box-sizing: border-box; `; - export const AddButton = styled.button` background-color: #008CBA; color: white; @@ -49,7 +50,7 @@ export const AddButton = styled.button` border: none; border-radius: 50%; cursor: pointer; - font-size: 1.5em; + font-size: 1.5em; font-weight: bold; display: flex; align-items: center; @@ -65,26 +66,28 @@ export const IconWrapper = styled.span` display: flex; align-items: center; justify-content: center; + line-height: 1; `; -export const DeleteAllButton = styled.button` - background-color: #dc3545; +export const StyledFunctionButton = styled.button` color: white; - width: 40%; - height: 45px; + flex-grow: 1; + height: 40px; border: none; border-radius: 4px; cursor: pointer; - font-size: 1.1em; - font-weight: bold; + font-size: 18px; display: flex; align-items: center; justify-content: center; - margin-top: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: background-color 0.2s ease-in-out; + + /* Dynamisk bakgrundsfärg baserat på '$type' prop */ + background-color: ${props => props.$type === 'delete' ? '#dc3545' : '#28a745'}; /* Röd för 'delete', grön för 'complete' */ &:hover { - background-color: #c82333; + background-color: ${props => props.$type === 'delete' ? '#c82333' : '#218838'}; } ${IconWrapper} { @@ -93,28 +96,24 @@ export const DeleteAllButton = styled.button` } `; -export const CompleteAllButton = styled.button` - background-color: rgb(19, 152, 19); - color: white; - width: 40%; - height: 45px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1.1em; - font-weight: bold; +export const ButtonRow = styled.div` display: flex; - align-items: center; - justify-content: center; - margin-top: 20px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + flex-wrap: wrap; + gap: 10px; + margin-top: 20px; + width: 100%; +`; + +export const ToggleListButton = styled(StyledFunctionButton)` + background-color: ${props => props.$active ? '#007bff' : '#6c757d'}; + flex-grow: 1; &:hover { - background-color: rgba(19, 152, 19, 0.57); + background-color: ${props => props.$active ? '#0056b3' : '#5a6268'}; } ${IconWrapper} { - margin-right: 8px; - font-size: 1.3em; + margin-right: 5px; + font-size: 1.1em; } `; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 0bd80eb..6fe8250 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,45 +1,62 @@ -import React from 'react'; -import { useTaskStore } from "../stores/useTaskStore"; -import { FaRegTrashAlt } from "react-icons/fa"; +import { useTaskStore } from '../stores/useTaskStore'; +import { FaRegTrashAlt, FaStar, FaRegStar } from 'react-icons/fa'; import { - StyledTaskList, - TaskItem, + TaskListContainer, + TaskItemWrapper, TaskText, - DeleteButton, TaskActions, - Icon, - CheckboxContainer, - TaskCheckbox + DeleteButton, + StarredButton, + Checkbox } from './TaskList.styles.jsx'; export const TaskList = () => { const tasks = useTaskStore((state) => state.tasks); const deleteTask = useTaskStore((state) => state.deleteTask); const completeTask = useTaskStore((state) => state.completeTask); + const toggleStarred = useTaskStore((state) => state.toggleStarred); + const showStarredOnly = useTaskStore((state) => state.showStarredOnly); + + const filteredTasks = showStarredOnly + ? tasks.filter(task => task.isStarred) + : tasks; return ( - - {tasks.map((task) => ( - - - completeTask(task.id)} - /> - {task.text} - + + {filteredTasks.length === 0 && ( +

+ {showStarredOnly ? 'No starred tasks yet!' : 'No tasks to show yet. Add a new one!'} +

+ )} + + {filteredTasks.map((task) => ( + + + completeTask(task.id)} + /> + completeTask(task.id)}> + {task.text} + - deleteTask(task.id)} title="Delete Task"> - - + toggleStarred(task.id)} + $isStarred={task.isStarred} + title={task.isStarred ? "Remove star" : "Star this task"} + > + {task.isStarred ? : } + + + deleteTask(task.id)} title="Delete task"> + -
+ ))} -
+ ); }; \ No newline at end of file diff --git a/src/components/TaskList.styles.jsx b/src/components/TaskList.styles.jsx index 20c447a..ead496a 100644 --- a/src/components/TaskList.styles.jsx +++ b/src/components/TaskList.styles.jsx @@ -1,85 +1,85 @@ import styled from 'styled-components'; -export const StyledTaskList = styled.ul` +export const TaskListContainer = styled.ul` list-style: none; padding: 0; - margin-top: 20px; - max-width: 600px; - margin-left: auto; - margin-right: auto; + margin: 20px 0; + width: 100%; `; -export const TaskItem = styled.li` +export const TaskItemWrapper = styled.li` display: flex; align-items: center; justify-content: space-between; - background-color: #f9f9f9; - border: 1px solid #eee; + background-color: #f8f8f8; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 10px; padding: 10px 15px; - margin-bottom: 8px; - border-radius: 5px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - min-height: 40px; - box-sizing: border-box; -`; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: background-color 0.2s ease-in-out; -export const CheckboxContainer = styled.div` - display: flex; - align-items: center; - flex-grow: 1; - cursor: pointer; - padding-right: 10px; + &.completed { + background-color: #e6ffe6; + text-decoration: line-through; + color: #666; + } `; -export const TaskCheckbox = styled.input` - width: 20px; - height: 20px; +export const TaskText = styled.span` + flex-grow: 1; margin-right: 10px; + font-size: 1.1em; + word-break: break-word; cursor: pointer; - flex-shrink: 0; `; -export const TaskText = styled.span` - text-decoration: ${(props) => (props.$isCompleted ? 'line-through' : 'unset')}; - color: ${(props) => (props.$isCompleted ? '#888' : 'inherit')}; - word-break: break-word; - font-size: 1em; - line-height: 1.4; +export const TaskActions = styled.div` + display: flex; + gap: 8px; + align-items: center; `; -export const TaskActionButton = styled.button` - padding: 8px; - width: 40px; - height: 40px; +export const DeleteButton = styled.button` + background-color: #dc3545; + color: white; border: none; border-radius: 50%; - cursor: pointer; - font-weight: bold; + width: 30px; + height: 30px; display: flex; align-items: center; justify-content: center; - font-size: 1.2em; + cursor: pointer; + font-size: 1em; flex-shrink: 0; &:hover { - opacity: 0.8; + background-color: #c82333; } `; -export const DeleteButton = styled(TaskActionButton)` - background-color: #f44336; - color: white; -`; - -export const TaskActions = styled.div` +export const StarredButton = styled.button` + background: none; + border: none; + color: ${props => props.$isStarred ? 'gold' : '#ccc'}; + cursor: pointer; + font-size: 1.3em; display: flex; - gap: 5px; - flex-shrink: 0; align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + + &:hover { + color: gold; + } `; -export const Icon = styled.span` - display: flex; - align-items: center; - justify-content: center; +export const Checkbox = styled.input` + margin-right: 10px; + width: 20px; + height: 20px; + cursor: pointer; + flex-shrink: 0; `; \ No newline at end of file diff --git a/src/stores/useTaskStore.js b/src/stores/useTaskStore.js index 90f5b08..bd37322 100644 --- a/src/stores/useTaskStore.js +++ b/src/stores/useTaskStore.js @@ -5,7 +5,10 @@ export const useTaskStore = create((set) => ({ addTask: (text) => set((state) => ({ - tasks: [...state.tasks, { id: Date.now(), text, isCompleted: false }], + tasks: [ + ...state.tasks, + { id: Date.now(), text, isCompleted: false, isStarred: false }, + ], })), deleteTask: (id) => @@ -18,12 +21,26 @@ export const useTaskStore = create((set) => ({ tasks: state.tasks.map((task) => task.id === id ? { ...task, isCompleted: !task.isCompleted } : task ), + })), + + completeAllTasks: () => + set((state) => ({ + tasks: state.tasks.map((task) => ({ ...task, isCompleted: true })), + })), - completeAllTasks: () => - set((state) => ({ - tasks: state.tasks.map((task) => ({ ...task, isCompleted: true })), - })), + removeAllTasks: () => set({ tasks: [] }), - removeAllTasks: () => set({ tasks: [] }), + toggleStarred: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, isStarred: !task.isStarred } : task + ), + })), + + showStarredOnly: false, + + toggleShowStarredOnly: () => + set((state) => ({ + showStarredOnly: !state.showStarredOnly, })), -})); \ No newline at end of file +})); From 8d452ea09824464f38a90d8a71125a40207417a7 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 23 May 2025 10:52:11 +0200 Subject: [PATCH 06/15] what --- src/App.css | 8 +++ src/components/TaskForm.jsx | 61 +++++++--------- src/components/TaskForm.styles.jsx | 32 ++------- src/components/TaskList.jsx | 111 +++++++++++++++++++---------- src/components/TaskList.styles.jsx | 15 +++- src/stores/useTaskStore.js | 107 +++++++++++++++------------ 6 files changed, 192 insertions(+), 142 deletions(-) diff --git a/src/App.css b/src/App.css index e69de29..a7fc29b 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,8 @@ +body { + background: white; + font-family: arial; +} + +p { + font-size: 18px; +} \ No newline at end of file diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index 23faf92..b7d0254 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -1,9 +1,9 @@ - import { useState } from "react"; import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; + import { SlPlus } from "react-icons/sl"; -import { FaRegTrashAlt, FaCheckCircle, FaStar, FaRegStar } from 'react-icons/fa'; +import { FaRegTrashAlt, FaCheckCircle, FaRegCheckCircle } from 'react-icons/fa'; import { FormContainer, StyledForm, @@ -13,18 +13,16 @@ import { IconWrapper, StyledFunctionButton, ButtonRow, - ToggleListButton } from './TaskForm.styles.jsx'; export const TaskForm = () => { const [taskValue, setTaskValue] = useState(""); const addTask = useTaskStore((state) => state.addTask); const completeAllTasks = useTaskStore((state) => state.completeAllTasks); + const uncompleteAllTasks = useTaskStore((state) => state.uncompleteAllTasks); const removeAllTasks = useTaskStore((state) => state.removeAllTasks); const tasks = useTaskStore((state) => state.tasks); - const showStarredOnly = useTaskStore((state) => state.showStarredOnly); - const toggleShowStarredOnly = useTaskStore((state) => state.toggleShowStarredOnly); const handleSubmit = (e) => { e.preventDefault(); @@ -34,8 +32,14 @@ export const TaskForm = () => { } }; - const handleCompleteAll = () => { + const handleToggleCompleteAll = () => { + const hasIncomplete = tasks.some(task => !task.isCompleted); + + if (hasIncomplete) { completeAllTasks(); + } else { + uncompleteAllTasks(); + } }; const handleDeleteAll = () => { @@ -44,24 +48,20 @@ export const TaskForm = () => { } }; - const handleToggleList = () => { - toggleShowStarredOnly(); - }; - - const hasIncompleteTasks = tasks.some(task => !task.isCompleted); + const allTasksCompleted = tasks.every(task => task.isCompleted); const hasAnyTasks = tasks.length > 0; return ( - Task + setTaskValue(e.target.value)} - placeholder="Add task..." + placeholder="Add new task" required /> @@ -73,32 +73,25 @@ export const TaskForm = () => { {hasAnyTasks && ( - {/* Button for toggling between all and starred tasks */} - - {showStarredOnly ? ( - <> Show all - ) : ( - <> Show starred - )} - + + {allTasksCompleted ? ( + + ) : ( + + )} + + {allTasksCompleted ? 'Unmark all' : 'Mark all as completed'} + - {/* "Mark everyone as complete" - only shown if there are incomplete tasks */} - {hasIncompleteTasks && ( - - Mark all as complete - - )} - - {tasks.length > 0 && ( // Show only if there are tasks + {tasks.length > 0 && ( - Delete All + Delete all )} - )} diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 6ce51bd..7fa1eb5 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -22,15 +22,8 @@ export const StyledForm = styled.form` `; export const FormLabel = styled.label` - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -`; + display: none; + `; export const FormInput = styled.input` flex-grow: 1; @@ -76,15 +69,16 @@ export const StyledFunctionButton = styled.button` border: none; border-radius: 4px; cursor: pointer; - font-size: 18px; + font-size: 16px; + font-weight: bold; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: background-color 0.2s ease-in-out; - /* Dynamisk bakgrundsfärg baserat på '$type' prop */ - background-color: ${props => props.$type === 'delete' ? '#dc3545' : '#28a745'}; /* Röd för 'delete', grön för 'complete' */ + + background-color: ${props => props.$type === 'delete' ? '#dc3545' : '#28a745'}; &:hover { background-color: ${props => props.$type === 'delete' ? '#c82333' : '#218838'}; @@ -102,18 +96,4 @@ export const ButtonRow = styled.div` gap: 10px; margin-top: 20px; width: 100%; -`; - -export const ToggleListButton = styled(StyledFunctionButton)` - background-color: ${props => props.$active ? '#007bff' : '#6c757d'}; - flex-grow: 1; - - &:hover { - background-color: ${props => props.$active ? '#0056b3' : '#5a6268'}; - } - - ${IconWrapper} { - margin-right: 5px; - font-size: 1.1em; - } `; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 6fe8250..62a88fd 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,3 +1,4 @@ +// src/components/TaskList.jsx import { useTaskStore } from '../stores/useTaskStore'; import { FaRegTrashAlt, FaStar, FaRegStar } from 'react-icons/fa'; @@ -7,8 +8,9 @@ import { TaskText, TaskActions, DeleteButton, - StarredButton, - Checkbox + StarredButton, + Checkbox, + EmptyListMessage } from './TaskList.styles.jsx'; export const TaskList = () => { @@ -16,47 +18,84 @@ export const TaskList = () => { const deleteTask = useTaskStore((state) => state.deleteTask); const completeTask = useTaskStore((state) => state.completeTask); const toggleStarred = useTaskStore((state) => state.toggleStarred); - const showStarredOnly = useTaskStore((state) => state.showStarredOnly); - const filteredTasks = showStarredOnly - ? tasks.filter(task => task.isStarred) - : tasks; + // Sort tasks based on the following criteria: + // 1. Allwys show completed tasks last. + // 2. Show starred tasks first among incomplete tasks. + // 3. Keeop the original order for tasks that are either all starred or all unstarred. + const sortedTasks = [...tasks].sort((a, b) => { + // Priority 1: Completed tasks last + // If 'a' is completed and 'b' is not, move 'a' to the end. + if (a.isCompleted && !b.isCompleted) { + return 1; + } + // If 'b' is completed and 'a' is not, move 'b' to the end. + if (!a.isCompleted && b.isCompleted) { + return -1; + } + + // If both tasks are either completed or not, we check for starred status. + // Then sort starred tasks first among incomplete tasks. + // Here we only compare starred status if both tasks are not completed. + if (!a.isCompleted && !b.isCompleted) { // Only compare starred status if both tasks are not completed + if (a.isStarred && !b.isStarred) { + return -1; // a is starred and b is not -> a comes first + } + if (!a.isStarred && b.isStarred) { + return 1; // b is starred and a is not -> b comes first + } + } + + // If neither task is starred or both are starred, we keep the original order. + // or both tasks are starred or both are not starred + return 0; + }); + + // Display the sorted tasks + const displayedTasks = sortedTasks; return ( - {filteredTasks.length === 0 && ( -

- {showStarredOnly ? 'No starred tasks yet!' : 'No tasks to show yet. Add a new one!'} -

+ {displayedTasks.length === 0 && ( + + No task added yet. Add one to get started! + )} - {filteredTasks.map((task) => ( - - - completeTask(task.id)} - /> - completeTask(task.id)}> - {task.text} - - - - toggleStarred(task.id)} - $isStarred={task.isStarred} - title={task.isStarred ? "Remove star" : "Star this task"} + {displayedTasks.map((task) => { + const checkboxId = `task-${task.id}`; + return ( + + completeTask(task.id)} + /> + completeTask(task.id)} > - {task.isStarred ? : } - - - deleteTask(task.id)} title="Delete task"> - - - - - ))} + {task.text} + + + + toggleStarred(task.id)} + $isStarred={task.isStarred} + title={task.isStarred ? "Remove star" : "Star this task"} + > + {task.isStarred ? : } + + + deleteTask(task.id)} title="Delete task"> + + + + + ); + })}
); }; \ No newline at end of file diff --git a/src/components/TaskList.styles.jsx b/src/components/TaskList.styles.jsx index ead496a..e672417 100644 --- a/src/components/TaskList.styles.jsx +++ b/src/components/TaskList.styles.jsx @@ -64,7 +64,7 @@ export const StarredButton = styled.button` border: none; color: ${props => props.$isStarred ? 'gold' : '#ccc'}; cursor: pointer; - font-size: 1.3em; + font-size: 22px; display: flex; align-items: center; justify-content: center; @@ -74,6 +74,13 @@ export const StarredButton = styled.button` &:hover { color: gold; } + + svg { + display: block; + margin: 0; + padding: 0; + line-height: 1; + } `; export const Checkbox = styled.input` @@ -82,4 +89,10 @@ export const Checkbox = styled.input` height: 20px; cursor: pointer; flex-shrink: 0; +`; + +export const EmptyListMessage = styled.p` + text-align: center; + color: #666; + margin: 20px 0; `; \ No newline at end of file diff --git a/src/stores/useTaskStore.js b/src/stores/useTaskStore.js index bd37322..be42052 100644 --- a/src/stores/useTaskStore.js +++ b/src/stores/useTaskStore.js @@ -1,46 +1,63 @@ +// src/stores/useTaskStore.js import { create } from "zustand"; - -export const useTaskStore = create((set) => ({ - tasks: [], - - addTask: (text) => - set((state) => ({ - tasks: [ - ...state.tasks, - { id: Date.now(), text, isCompleted: false, isStarred: false }, - ], - })), - - deleteTask: (id) => - set((state) => ({ - tasks: state.tasks.filter((task) => task.id !== id), - })), - - completeTask: (id) => - set((state) => ({ - tasks: state.tasks.map((task) => - task.id === id ? { ...task, isCompleted: !task.isCompleted } : task - ), - })), - - completeAllTasks: () => - set((state) => ({ - tasks: state.tasks.map((task) => ({ ...task, isCompleted: true })), - })), - - removeAllTasks: () => set({ tasks: [] }), - - toggleStarred: (id) => - set((state) => ({ - tasks: state.tasks.map((task) => - task.id === id ? { ...task, isStarred: !task.isStarred } : task - ), - })), - - showStarredOnly: false, - - toggleShowStarredOnly: () => - set((state) => ({ - showStarredOnly: !state.showStarredOnly, - })), -})); +import { devtools, persist } from "zustand/middleware"; + +export const useTaskStore = create( + devtools( + persist( + (set) => ({ + tasks: [], + + addTask: (text) => + set((state) => ({ + tasks: [ + ...state.tasks, + { + id: Date.now(), + text, + isCompleted: false, + isStarred: false, + }, + ], + })), + + deleteTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((task) => task.id !== id), + })), + + completeTask: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id + ? { ...task, isCompleted: !task.isCompleted } + : task + ), + })), + + completeAllTasks: () => + set((state) => ({ + tasks: state.tasks.map((task) => ({ ...task, isCompleted: true })), + })), + + + uncompleteAllTasks: () => + set((state) => ({ + tasks: state.tasks.map((task) => ({ ...task, isCompleted: false })), + })), + + removeAllTasks: () => set({ tasks: [] }), + + toggleStarred: (id) => + set((state) => ({ + tasks: state.tasks.map((task) => + task.id === id ? { ...task, isStarred: !task.isStarred } : task + ), + })), + }), + { + name: "task-storage", + } + ) + ) +); From a11c28b3f506cdff731f5068baadfa06f6fceeb5 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 23 May 2025 14:08:40 +0200 Subject: [PATCH 07/15] added counter, picture of no tasks --- index.html | 2 +- package.json | 1 + public/empty.png | Bin 0 -> 10817 bytes public/favicon.png | Bin 0 -> 14559 bytes public/vite.svg | 1 - src/App.css | 11 +++++++++++ src/components/Count.jsx | 20 +++++++++++++------- src/components/Header.jsx | 4 ++-- src/components/TaskForm.jsx | 5 +++-- src/components/TaskForm.styles.jsx | 9 +++++---- src/components/TaskList.jsx | 21 +++++++++++++++------ src/components/TaskList.styles.jsx | 7 +++++++ src/stores/useTaskStore.js | 2 +- 13 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 public/empty.png create mode 100644 public/favicon.png delete mode 100644 public/vite.svg diff --git a/index.html b/index.html index a14f979..0b31521 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Make it happen! diff --git a/package.json b/package.json index 4e07c1f..0094891 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "moment": "^2.30.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.5.0", diff --git a/public/empty.png b/public/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..83ff1e642ab67f01f9a6d7316d169906355832ea GIT binary patch literal 10817 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 literal 0 HcmV?d00001 diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.css b/src/App.css index a7fc29b..7796b7d 100644 --- a/src/App.css +++ b/src/App.css @@ -5,4 +5,15 @@ body { p { font-size: 18px; +} + +.image-wrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.image-wrapper img { + width: 150px; + margin: 20px; } \ No newline at end of file diff --git a/src/components/Count.jsx b/src/components/Count.jsx index 387da24..cea0d85 100644 --- a/src/components/Count.jsx +++ b/src/components/Count.jsx @@ -1,10 +1,16 @@ -import useTaskStore from "../stores/useTaskStore"; +import { useTaskStore } from "../stores/useTaskStore"; -const Counter = () => { +export const Count = () => { const tasks = useTaskStore((state) => state.tasks); - const notCompleted = tasks.filter((task) => !task.completed).length; + const completed = tasks.filter((task) => task.isCompleted).length; + const totalTasks = tasks.length; + const starredTasks = tasks.filter((task) => task.isStarred).length; - return

Uncompleted tasks: {notCompleted}

; -} - -export default Counter; \ No newline at end of file + return ( + <> +

+ Completed tasks: {completed} / {totalTasks}

+

Starred tasks: {starredTasks}

+ + ); +}; \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 1321685..e962c8b 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -19,7 +19,7 @@ export const Header = styled.header` export const HeaderText = styled.h1` font-size: 2.5em; - color:rgb(24, 148, 183); + color:rgb(0, 0, 0); margin: 0; text-align: center; @@ -50,7 +50,7 @@ export const HeaderComponent = () => { return (
Make it happen - A ToDo-app created by Cathi + A simple but efficent to-do app
); } \ No newline at end of file diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index b7d0254..0f5de6e 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { useTaskStore } from "../stores/useTaskStore"; import { TaskList } from "./TaskList"; +import { Count } from "./Count" -import { SlPlus } from "react-icons/sl"; import { FaRegTrashAlt, FaCheckCircle, FaRegCheckCircle } from 'react-icons/fa'; import { FormContainer, @@ -65,9 +65,10 @@ export const TaskForm = () => { required /> - + + + diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 7fa1eb5..283a392 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; export const FormContainer = styled.div` padding: 20px; - background-color: #e0f2f7; + background-color:rgb(237, 239, 239); border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); margin-bottom: 20px; @@ -64,7 +64,8 @@ export const IconWrapper = styled.span` export const StyledFunctionButton = styled.button` color: white; - flex-grow: 1; + width: 40%; + flex-grow: 1; height: 40px; border: none; border-radius: 4px; @@ -78,10 +79,10 @@ export const StyledFunctionButton = styled.button` transition: background-color 0.2s ease-in-out; - background-color: ${props => props.$type === 'delete' ? '#dc3545' : '#28a745'}; + background-color: ${props => props.$type === 'delete' ? "#c82333" : "#066028"}; &:hover { - background-color: ${props => props.$type === 'delete' ? '#c82333' : '#218838'}; + background-color: ${props => props.$type === 'delete' ? "#dc3545" : '#218838'}; } ${IconWrapper} { diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 62a88fd..deda07f 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,6 +1,7 @@ // src/components/TaskList.jsx import { useTaskStore } from '../stores/useTaskStore'; import { FaRegTrashAlt, FaStar, FaRegStar } from 'react-icons/fa'; +import moment from "moment"; import { TaskListContainer, @@ -10,7 +11,8 @@ import { DeleteButton, StarredButton, Checkbox, - EmptyListMessage + EmptyListMessage, + TimestampText } from './TaskList.styles.jsx'; export const TaskList = () => { @@ -42,22 +44,26 @@ export const TaskList = () => { return -1; // a is starred and b is not -> a comes first } if (!a.isStarred && b.isStarred) { - return 1; // b is starred and a is not -> b comes first + return 1; } } - - // If neither task is starred or both are starred, we keep the original order. - // or both tasks are starred or both are not starred + // Om ingen annan sortering, sortera efter createdAt för att visa de senast tillagda först + // Använd bara createdAt om båda har det (för att hantera äldre uppgifter utan timestamp) + if (a.createdAt && b.createdAt) { + return new Date(b.createdAt) - new Date(a.createdAt); // Nyaste först + } return 0; }); - // Display the sorted tasks const displayedTasks = sortedTasks; return ( {displayedTasks.length === 0 && ( +
+ Picture of no tasks +
No task added yet. Add one to get started!
)} @@ -79,6 +85,9 @@ export const TaskList = () => { > {task.text} + + {moment(task.createdAt).format("YYYY-MM-DD")} + ({ ...task, isCompleted: true })), })), - uncompleteAllTasks: () => set((state) => ({ tasks: state.tasks.map((task) => ({ ...task, isCompleted: false })), From 5c74bd919756128e64d90bf18d688afb410e1bbe Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 23 May 2025 22:22:12 +0200 Subject: [PATCH 08/15] omg so ambivalent, changed alot, still not satisfied --- src/App.css | 27 ++++++++++- src/App.jsx | 1 - src/components/Count.jsx | 9 ++-- src/components/Footer.jsx | 0 src/components/Header.jsx | 17 +------ src/components/TaskForm.jsx | 6 ++- src/components/TaskForm.styles.jsx | 25 ++++++---- src/components/TaskList.jsx | 74 ++++++++---------------------- src/components/TaskList.styles.jsx | 24 ++++++++-- src/utils/sortTasks.js | 17 +++++++ 10 files changed, 107 insertions(+), 93 deletions(-) delete mode 100644 src/components/Footer.jsx create mode 100644 src/utils/sortTasks.js diff --git a/src/App.css b/src/App.css index 7796b7d..8093770 100644 --- a/src/App.css +++ b/src/App.css @@ -5,6 +5,7 @@ body { p { font-size: 18px; + margin: 0; } .image-wrapper { @@ -16,4 +17,28 @@ p { .image-wrapper img { width: 150px; margin: 20px; -} \ No newline at end of file +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.count-container { + display: flex; + flex-direction: column; + gap: 5px; +} + +@media (min-width: 768px) { + .count-container { + flex-direction: row; + justify-content: space-between; + } +} diff --git a/src/App.jsx b/src/App.jsx index fcb962d..82f3d0b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,5 @@ import { TaskForm } from "./components/TaskForm"; import { HeaderComponent } from "./components/Header"; - import "./App.css" export const App = () => { diff --git a/src/components/Count.jsx b/src/components/Count.jsx index cea0d85..858da8b 100644 --- a/src/components/Count.jsx +++ b/src/components/Count.jsx @@ -7,10 +7,9 @@ export const Count = () => { const starredTasks = tasks.filter((task) => task.isStarred).length; return ( - <> -

- Completed tasks: {completed} / {totalTasks}

-

Starred tasks: {starredTasks}

- +
+

Completed tasks: {completed} / {totalTasks}

+

Starred tasks: {starredTasks}

+
); }; \ No newline at end of file diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/Header.jsx b/src/components/Header.jsx index e962c8b..a1d5dcf 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -31,26 +31,11 @@ export const HeaderText = styled.h1` font-size: 1.5em; } ` -export const HeaderSubtitle = styled.h2` - font-size: 1em; - color:rgb(0, 0, 0); - margin-top: 10px; - text-align: center; - - @media (max-width: 600px) { - font-size: 1.2em; - } - - @media (max-width: 400px) { - font-size: 1em; - } -`; export const HeaderComponent = () => { return (
- Make it happen - A simple but efficent to-do app + TODO LIST
); } \ No newline at end of file diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx index 0f5de6e..933f43f 100644 --- a/src/components/TaskForm.jsx +++ b/src/components/TaskForm.jsx @@ -54,7 +54,9 @@ export const TaskForm = () => { return ( - + + Task input + { )} - {allTasksCompleted ? 'Unmark all' : 'Mark all as completed'} + {allTasksCompleted ? 'Unmark all' : 'Complete all'} {tasks.length > 0 && ( diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index 283a392..bb494a7 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -21,9 +21,9 @@ export const StyledForm = styled.form` margin-bottom: 20px; `; -export const FormLabel = styled.label` - display: none; - `; +export const FormLabel = styled.label.attrs(() => ({ + className: 'visually-hidden' +}))``; export const FormInput = styled.input` flex-grow: 1; @@ -36,7 +36,7 @@ export const FormInput = styled.input` `; export const AddButton = styled.button` - background-color: #008CBA; + background-color:rgb(75, 24, 203); color: white; width: 40px; height: 40px; @@ -51,7 +51,7 @@ export const AddButton = styled.button` flex-shrink: 0; &:hover { - background-color: #007bb5; + background-color: gray; } `; @@ -79,10 +79,10 @@ export const StyledFunctionButton = styled.button` transition: background-color 0.2s ease-in-out; - background-color: ${props => props.$type === 'delete' ? "#c82333" : "#066028"}; + background-color: ${props => props.type === 'delete' ? "rgb(75, 24, 203);" : "rgb(75, 24, 203);"}; &:hover { - background-color: ${props => props.$type === 'delete' ? "#dc3545" : '#218838'}; + background-color: ${props => props.type === 'delete' ? "rgb(75, 24, 203);" : 'gray'}; } ${IconWrapper} { @@ -90,11 +90,18 @@ export const StyledFunctionButton = styled.button` font-size: 1.3em; } `; - export const ButtonRow = styled.div` display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; - width: 100%; + width: 100%; + + @media (max-width: 767px) { + flex-direction: column; + + button { + width: 100%; + } + } `; \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index deda07f..aabc17b 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,5 +1,5 @@ -// src/components/TaskList.jsx import { useTaskStore } from '../stores/useTaskStore'; +import { sortTasks } from '../utils/sortTasks.js'; import { FaRegTrashAlt, FaStar, FaRegStar } from 'react-icons/fa'; import moment from "moment"; @@ -12,7 +12,8 @@ import { StarredButton, Checkbox, EmptyListMessage, - TimestampText + TimestampText, + TaskTextWrapper } from './TaskList.styles.jsx'; export const TaskList = () => { @@ -21,45 +22,12 @@ export const TaskList = () => { const completeTask = useTaskStore((state) => state.completeTask); const toggleStarred = useTaskStore((state) => state.toggleStarred); - // Sort tasks based on the following criteria: - // 1. Allwys show completed tasks last. - // 2. Show starred tasks first among incomplete tasks. - // 3. Keeop the original order for tasks that are either all starred or all unstarred. - const sortedTasks = [...tasks].sort((a, b) => { - // Priority 1: Completed tasks last - // If 'a' is completed and 'b' is not, move 'a' to the end. - if (a.isCompleted && !b.isCompleted) { - return 1; - } - // If 'b' is completed and 'a' is not, move 'b' to the end. - if (!a.isCompleted && b.isCompleted) { - return -1; - } - // If both tasks are either completed or not, we check for starred status. - // Then sort starred tasks first among incomplete tasks. - // Here we only compare starred status if both tasks are not completed. - if (!a.isCompleted && !b.isCompleted) { // Only compare starred status if both tasks are not completed - if (a.isStarred && !b.isStarred) { - return -1; // a is starred and b is not -> a comes first - } - if (!a.isStarred && b.isStarred) { - return 1; - } - } - // Om ingen annan sortering, sortera efter createdAt för att visa de senast tillagda först - // Använd bara createdAt om båda har det (för att hantera äldre uppgifter utan timestamp) - if (a.createdAt && b.createdAt) { - return new Date(b.createdAt) - new Date(a.createdAt); // Nyaste först - } - return 0; - }); - - const displayedTasks = sortedTasks; + const sortedTasks = sortTasks(tasks); return ( - {displayedTasks.length === 0 && ( + {sortedTasks.length === 0 && (
Picture of no tasks @@ -68,26 +36,24 @@ export const TaskList = () => { )} - {displayedTasks.map((task) => { + {sortedTasks.map((task) => { const checkboxId = `task-${task.id}`; return ( - completeTask(task.id)} - /> - completeTask(task.id)} - > - {task.text} - - - {moment(task.createdAt).format("YYYY-MM-DD")} - + + + completeTask(task.id)} + /> + {task.text || "Unnamed task"} + + + {moment(task.createdAt).format("YYYY-MM-DD")} + + { + return [...tasks].sort((a, b) => { + if (a.isCompleted && !b.isCompleted) return 1; + if (!a.isCompleted && b.isCompleted) return -1; + + if (!a.isCompleted && !b.isCompleted) { + if (a.isStarred && !b.isStarred) return -1; + if (!a.isStarred && b.isStarred) return 1; + } + + if (a.createdAt && b.createdAt) { + return new Date(b.createdAt) - new Date(a.createdAt); + } + + return 0; + }); +}; From 2f5c60f6684942cdf65bb40edd3275aeced4f6ac Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 23 May 2025 22:58:56 +0200 Subject: [PATCH 09/15] styling footer etc --- src/App.jsx | 22 +++++++++- src/components/Footer.jsx | 65 ++++++++++++++++++++++++++++++ src/components/Header.jsx | 9 ++++- src/components/TaskForm.styles.jsx | 3 -- 4 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 src/components/Footer.jsx diff --git a/src/App.jsx b/src/App.jsx index 82f3d0b..2db2951 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,30 @@ +import styled from "styled-components"; import { TaskForm } from "./components/TaskForm"; import { HeaderComponent } from "./components/Header"; +import { FooterComponent } from "./components/Footer"; import "./App.css" +const CardWrapper = styled.section` + background-color: rgb(237, 239, 239); + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + max-width: 1000px; + margin: 50px auto; + + @media (max-width: 768px) { + margin: 0px; + } +`; + export const App = () => { return ( <> - - + + + + + ) } diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx new file mode 100644 index 0000000..3055f58 --- /dev/null +++ b/src/components/Footer.jsx @@ -0,0 +1,65 @@ +import styled from "styled-components"; +import { FaGithub } from "react-icons/fa"; + +export const Footer = styled.footer` + padding: 20px; + background-color: rgb(237, 239, 239); + max-width: 600px; + margin-left: auto; + margin-right: auto; + margin-bottom: 50px; /* istället för margin-top */ + box-sizing: border-box; + + text-align: center; + font-size: 1.3rem; + font-weight: bold; + + @media (max-width: 600px) { + font-size: 1.5rem; + } + + @media (max-width: 400px) { + font-size: 1.2rem; + } +`; + +const FooterText = styled.p` + font-size: 1em; + text-align: center; + color: #333; + + a { + color: #333; + text-decoration: none; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 4px; + + &:hover { + text-decoration: none; + } + + svg { + font-size: 1.1em; + padding-right: 5px; + } + } +`; + +export const FooterComponent = () => { + return ( +
+ + © 2025 Designed and created {" "} + + Cathi + + +
+ ); +}; \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index a1d5dcf..bff9a57 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,9 +1,13 @@ import styled from "styled-components"; export const Header = styled.header` - background-color:rgb(255, 255, 255); padding: 20px; - color: white; + background-color:rgb(237, 239, 239); + max-width: 600px; + margin-left: auto; + margin-right: auto; + margin-top: 50px; + box-sizing: border-box; text-align: center; font-size: 1.3rem; @@ -22,6 +26,7 @@ export const HeaderText = styled.h1` color:rgb(0, 0, 0); margin: 0; text-align: center; + letter-spacing: 5px; @media (max-width: 600px) { font-size: 2em; diff --git a/src/components/TaskForm.styles.jsx b/src/components/TaskForm.styles.jsx index bb494a7..e089409 100644 --- a/src/components/TaskForm.styles.jsx +++ b/src/components/TaskForm.styles.jsx @@ -4,9 +4,6 @@ import styled from 'styled-components'; export const FormContainer = styled.div` padding: 20px; background-color:rgb(237, 239, 239); - border-radius: 8px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - margin-bottom: 20px; max-width: 600px; margin-left: auto; margin-right: auto; From c8978d71507a7a817164dc51a890b22b43785c26 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sun, 25 May 2025 08:50:04 +0200 Subject: [PATCH 10/15] testing accessibility --- src/components/Footer.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 3055f58..4e9f18e 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -23,8 +23,8 @@ export const Footer = styled.footer` } `; -const FooterText = styled.p` - font-size: 1em; +const FooterText = styled.h2` + font-size: 16px; text-align: center; color: #333; @@ -51,7 +51,7 @@ export const FooterComponent = () => { return (