diff --git a/package-lock.json b/package-lock.json index 5c1b936d..8ddffb5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,14 @@ "name": "project-todos-context", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "styled-components": "^6.1.13", + "zustand": "^5.0.1" }, "devDependencies": { "@types/react": "^18.2.15", @@ -346,6 +351,27 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "cpu": [ @@ -427,6 +453,52 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz", + "integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz", + "integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", + "integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", + "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "dev": true, @@ -578,12 +650,12 @@ }, "node_modules/@types/prop-types": { "version": "15.7.10", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.2.37", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -601,7 +673,13 @@ }, "node_modules/@types/scheduler": { "version": "0.16.6", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", "license": "MIT" }, "node_modules/@ungap/structured-clone": { @@ -866,6 +944,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001561", "dev": true, @@ -934,9 +1021,30 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { - "version": "3.1.2", - "dev": true, + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/debug": { @@ -2255,7 +2363,6 @@ }, "node_modules/nanoid": { "version": "3.3.7", - "dev": true, "funding": [ { "type": "github", @@ -2282,7 +2389,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2472,12 +2578,12 @@ }, "node_modules/picocolors": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/postcss": { - "version": "8.4.31", - "dev": true, + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -2494,14 +2600,20 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -2512,7 +2624,6 @@ }, "node_modules/prop-types": { "version": "15.8.1", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -2570,7 +2681,6 @@ }, "node_modules/react-is": { "version": "16.13.1", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { @@ -2800,6 +2910,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -2833,8 +2949,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2923,6 +3040,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "dev": true, @@ -2958,6 +3109,12 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -3253,6 +3410,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index a23290e8..a53bf1ed 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-solid-svg-icons": "^6.7.1", + "@fortawesome/react-fontawesome": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "styled-components": "^6.1.13", + "zustand": "^5.0.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/src/App.jsx b/src/App.jsx index 496ab1b1..6eab6be2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,11 @@ -export const App = () => { - return
Find me in App.jsx!
; -}; +import MyToDoListPage from "./Page/MyToDoListPage"; + +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/src/Page/MyToDoListPage.jsx b/src/Page/MyToDoListPage.jsx new file mode 100644 index 00000000..693b14bf --- /dev/null +++ b/src/Page/MyToDoListPage.jsx @@ -0,0 +1,67 @@ +import ToDoList from '../components/ToDoList'; +import AddToDoForm from '../components/AddToDoForm'; +import Counter from '../components/Counter'; +import styled from "styled-components"; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #4c657e; + color: #fff; + width: 100vw; + height: 100vh; + padding: 20px; + box-sizing: border-box; + border-radius: 100px; + overflow: hidden; + + + @media (min-width: 1024px) { + width: 36vw; + height: 70vh; + padding: 40px; + border-radius: 30px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +`; + +const StyledToDoList = styled.div` +width: 100%; +height: 100 %; +overflow - y: auto; +padding: 10px; +`; + + + +const StyledFormContainer = styled.div` + position: sticky; + bottom: 0; + width: 100%; + background-color: #4c657e; + padding: 10px 15px; + + z-index: 1; +`; + +const MyToDoListPage = () => { + return ( + +

My To-Do List

+ + + + + + + +
+ ); +}; + +export default MyToDoListPage; diff --git a/src/components/AddToDoForm.jsx b/src/components/AddToDoForm.jsx new file mode 100644 index 00000000..e2f55328 --- /dev/null +++ b/src/components/AddToDoForm.jsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import useTodoStore from "../stores/store"; +import styled from "styled-components"; + + +const StyledForm = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + margin-top: 20px; +`; + +const StyledInput = styled.input` + width: 100%; + max-width: 400px; + padding: 10px 15px; + font-size: 16px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + outline: none; + background: #f9f9f9; + color: #333; + + &:focus { + border-color: #007bff; + } +`; + +const StyledButton = styled.button` + padding: 10px 20px; + font-size: 16px; + color: #fff; + background-color: black; + border: none; + border-radius: 20px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #d9363e; + } +`; + + +const AddToDoForm = () => { + const [text, setText] = useState(""); + const addTodo = useTodoStore((state) => state.addTodo); + + const handleSubmit = (e) => { + e.preventDefault(); + if (text.trim()) { + addTodo(text); + setText(""); + } + }; + + return ( + + setText(e.target.value)} + placeholder="Add a new task.." + /> + Add + + ); +} + +export default AddToDoForm; diff --git a/src/components/ToDoList.jsx b/src/components/ToDoList.jsx new file mode 100644 index 00000000..6948336c --- /dev/null +++ b/src/components/ToDoList.jsx @@ -0,0 +1,77 @@ +import styled from "styled-components"; +import useTodoStore from "../stores/store"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; + + +const List = styled.ul` + list-style: none; + padding: 0; + margin: 20px auto; + max-width: 600px; + color: black; +`; + +const ListItem = styled.li` + display: flex; + align-items: center; + justify-content: space-between; + background-color: #f9f9f9; + padding: 10px 15px; + margin-bottom: 10px; + border-radius: 10px; + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1); +`; + +const Label = styled.label` + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + text-decoration: ${(props) => (props.completed ? "line-through" : "none")}; + color: ${(props) => (props.completed ? "#000000" : "#010101")}; +`; + +const Checkbox = styled.input` + cursor: pointer; +`; + +const RemoveIcon = styled(FontAwesomeIcon)` + color: black; + font-size: 18px; + cursor: pointer; + transition: color 0.3s ease; + + &:hover { + color: #d9363e; + } +`; + +const ToDoList = () => { + const todos = useTodoStore((state) => state.todos); + const toggleTodo = useTodoStore((state) => state.toggleTodo); + const removeTodo = useTodoStore((state) => state.removeTodo); + + return ( + + {todos.map((todo) => ( + + + removeTodo(todo.id)} + /> + + ))} + + ); +} + +export default ToDoList; diff --git a/src/components/counter.jsx b/src/components/counter.jsx new file mode 100644 index 00000000..a39a37aa --- /dev/null +++ b/src/components/counter.jsx @@ -0,0 +1,12 @@ +// counter.jsx + +import useTodoStore from "../stores/store"; + +const Counter = () => { + const todos = useTodoStore((state) => state.todos); + const uncompletedCount = todos.filter((todo) => !todo.completed).length; + + return

Uncompleted tasks: {uncompletedCount}

; +} + +export default Counter; diff --git a/src/index.css b/src/index.css index 4669a352..d9b57f51 100644 --- a/src/index.css +++ b/src/index.css @@ -11,4 +11,9 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; +} + +:root { + + background-color: #36413d; } \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 51294f39..b91620d3 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { App } from "./App.jsx"; +import App from "./App.jsx"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/src/stores/Store.js b/src/stores/Store.js new file mode 100644 index 00000000..2554ec75 --- /dev/null +++ b/src/stores/Store.js @@ -0,0 +1,36 @@ +// Store.jsx + +import { create } from 'zustand'; + +const useTodoStore = create((set) => ({ // Creates a store using `create`. The function takes `set`, used to update the state. + + todos: [ // Defines the initial state, an array of objects representing tasks. + { id: "1", text: "Buy groceries", completed: false }, // Task 1 + { id: "2", text: "Walk the dog", completed: true }, // Task 2 + ], + + addTodo: (text) => // Function to add a new task. + set((state) => ({ // Updates the state using `set`. + todos: [...state.todos, { id: Date.now().toString(), text, completed: false }], + // Copies existing tasks (`...state.todos`) and adds a new task: + // - `id`: Created with `Date.now()` to ensure uniqueness. + // - `text`: The text passed in from the form. + // - `completed`: Defaults to `false` as new tasks are always incomplete. + })), + + removeTodo: (id) => // Function to remove a task. + set((state) => ({ // Updates the state using `set`. + todos: state.todos.filter((todo) => todo.id !== id), // Filters out the task with the matching `id`. + })), + + toggleTodo: (id) => // Function to toggle between `completed: true` and `completed: false`. + set((state) => ({ // Updates the state using `set`. + todos: state.todos.map((todo) => // Iterates through all tasks. + todo.id === id // If the task's ID matches the given ID: + ? { ...todo, completed: !todo.completed } // Toggle the `completed` property. + : todo // If the ID doesn't match, return the task as is. + ), + })), +})); + +export default useTodoStore;