diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index a124b9a7..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 100, - "tabWidth": 2, - "trailingComma": "all", - "singleQuote": true, - "jsxSingleQuote": true, - "semi": false - } \ No newline at end of file diff --git a/README.md b/README.md index a2f13c0b..4e61fad7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD # ๐Ÿ–ฅ๏ธ Mission-FE-Zero100 FE Zero100 ๋ฏธ์…˜์„ ์œ„ํ•œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index b157c90c..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,43 +0,0 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import prettier from 'eslint-config-prettier'; // Prettier ์„ค์ • ์ถ”๊ฐ€ -import prettierPlugin from 'eslint-plugin-prettier'; // Prettier ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ -export default [ - { ignores: ['dist'] }, // 'dist' ๋””๋ ‰ํ† ๋ฆฌ๋Š” ESLint ๊ฒ€์‚ฌ๋ฅผ ๋ฌด์‹œ - { - files: ['**/*.{js,jsx}'], // ESLint๊ฐ€ ๊ฒ€์‚ฌํ•  ํŒŒ์ผ ํ™•์žฅ์ž ์„ค์ • - languageOptions: { - ecmaVersion: 2020, // ECMAScript 2020 ์ง€์› - globals: globals.browser, // ๋ธŒ๋ผ์šฐ์ € ์ „์—ญ ๋ณ€์ˆ˜ ํ—ˆ์šฉ - parserOptions: { - ecmaVersion: 'latest', // ์ตœ์‹  ECMAScript ๋ฒ„์ „ ์ง€์› - ecmaFeatures: { jsx: true }, // JSX ์ง€์› - sourceType: 'module', // ECMAScript ๋ชจ๋“ˆ ์‚ฌ์šฉ - }, - }, - settings: { - react: { version: '18.3' }, // React ๋ฒ„์ „ ๊ฐ์ง€ - }, - plugins: { - react, // React ESLint ํ”Œ๋Ÿฌ๊ทธ์ธ - 'react-hooks': reactHooks, // React Hooks ํ”Œ๋Ÿฌ๊ทธ์ธ - 'react-refresh': reactRefresh, // React Fast Refresh ํ”Œ๋Ÿฌ๊ทธ์ธ - prettier: prettierPlugin, // Prettier ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ - }, - rules: { - ...js.configs.recommended.rules, // ESLint ์ถ”์ฒœ ๊ทœ์น™ ์ ์šฉ - ...react.configs.recommended.rules, // React ์ถ”์ฒœ ๊ทœ์น™ ์ ์šฉ - ...react.configs['jsx-runtime'].rules, // JSX ๋Ÿฐํƒ€์ž„ ๊ด€๋ จ ๊ทœ์น™ - ...reactHooks.configs.recommended.rules, // React Hooks ์ถ”์ฒœ ๊ทœ์น™ ์ ์šฉ - 'react/jsx-no-target-blank': 'off', // target="_blank" ๋ณด์•ˆ ๊ฒฝ๊ณ  ๋น„ํ™œ์„ฑํ™” - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - 'prettier/prettier': 'error', // Prettier ๊ทœ์น™์„ ์œ„๋ฐ˜ํ•˜๋ฉด ESLint์—์„œ ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ - }, - }, -]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b77564a5..ad96620a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.4.1" }, "devDependencies": { "@eslint/js": "^9.21.0", @@ -1339,6 +1340,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1589,6 +1596,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2432,6 +2448,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz", + "integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz", + "integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==", + "license": "MIT", + "dependencies": { + "react-router": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2505,6 +2561,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2564,6 +2626,12 @@ "node": ">=8" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 4fffc473..c050a39c 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,11 @@ "build": "vite build", "lint": "eslint .", "preview": "vite preview" - - - }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.4.1" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/App.jsx b/src/App.jsx index 9b836f3c..d82f27d3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,23 +1,50 @@ -import "./index.css"; -import Text from "./component/Text"; -import Input from "./component/Input"; -import Button from "./component/Button"; -import Checkbox from "./component/Checkbox"; +import React from "react"; +import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import TodoPage from "./component/TodoPage"; +import LoginPage from "./component/LoginPage"; +import SignupPage from "./component/SignupPage"; -function App() { +function App(){ return ( -
- {/* text part */} - + + + }/> + }/> + }/> + }/> + + + ); +} + +function Home(){ + const navigate =useNavigate(); + + const buttonStyle = { + width: "200px", + padding: "10px 0", + margin: "20px auto", + display: "block", + backgroundColor: "#ddd", + border: "none", + borderRadius: "30px", + fontSize: "1.1rem", + fontWeight: "600", + }; + + const titleStyle = { + fontSize: "2rem", + fontWeight: "700", + marginBottom: "40px", + }; - {/* input part */} - - {/* button part */} - +
); } -export default App; +export default App; \ No newline at end of file diff --git a/src/Blocks.jsx b/src/Blocks.jsx deleted file mode 100644 index 9095b707..00000000 --- a/src/Blocks.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import styled from "styled-components"; - -const Wrapper = styled.div` -padding: 1rem; -display: flex; -flex-direction: row; -align-items: flex-start; -justify-content: flex-start; -background-color: lightgrey; -`; - -const Block = styled.div` -padding: ${(props)=> props.padding}; -border: 1px solid black; -border-radius: 1rem; -background-color: ${(props) => props.backgroundColor}; -color: white; -font-size: 2rem; -font-weight: bold; -text-align: center; -`; - -const blockItems= [ - { - label: "1", - padding: "1rem", - backgroundColor: "red", - }, - { - label: "2", - padding: "3rem", - backgroundColor: "green", - }, - { - label:"3", - padding: "2rem", - backgroundColor: "blue", - }, -]; - -function Blocks(props){ - return( - - {blockItems.map((blockItem) => { - return ( - - {blockItem.label} - - ); - })} - - ); -} - -export default Blocks; \ No newline at end of file diff --git a/src/component/AddTodo.jsx b/src/component/AddTodo.jsx new file mode 100644 index 00000000..e855bb5a --- /dev/null +++ b/src/component/AddTodo.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import styled from "styled-components"; + +const AddTodoContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +`; + +const StyledText = styled.h2` + text-align: center; + display: flex; +`; + +const StyledInput = styled.input` + width: 500px; + padding: 14px; + border-radius: 0px; + margin-bottom: 8px; +`; + +const StyledButton = styled.button` + background-color: black; + color: white; + width: 532px; + padding: 9px; + border: none; + border-radius: 0px; + margin-bottom: 8px; +`; + +const AddTodo = ({ onAddTodo }) => { + const [inputValue, setInputValue] = useState(""); + + const handleInputChange = (e) => { + setInputValue(e.target.value); + }; + + const handleAddClick = () => { + onAddTodo(inputValue); + setInputValue(""); + }; + + return( + + What needs to be done? + + Add + + ) +} + +export default AddTodo; \ No newline at end of file diff --git a/src/component/Button.jsx b/src/component/Button.jsx deleted file mode 100644 index 551f49c4..00000000 --- a/src/component/Button.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' - -const Button = ({label, onClick}) => { - return ( -
- - - -
- ) -} - -export default Button; diff --git a/src/component/Category.jsx b/src/component/Category.jsx new file mode 100644 index 00000000..291e5d6b --- /dev/null +++ b/src/component/Category.jsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import styled from "styled-components"; + +const CategoryContainer = styled.div` + display: flex; + justify-content: center; + gap: 8px; +`; + +const StyledButton = styled.button` + width: 130px; + padding: 7px; + background-color: white; + border: 2px solid ${(props) => (props.active ? "black" : "#ccc")}; + border-radius: 0px; + font-size: 14px; +`; + +const Category = ({ activeCategory, setActiveCategory }) => { + return ( + + {["All", "Active", "Completed"].map((label) => ( + setActiveCategory(label)} + > + {label} + + ))} + + ); +}; + +export default Category; \ No newline at end of file diff --git a/src/component/Checkbox.jsx b/src/component/Checkbox.jsx deleted file mode 100644 index e674f743..00000000 --- a/src/component/Checkbox.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' - -const Checkbox = () => { - return ( -
-

3 tasks remaining

- -
- ) -} - -export default Checkbox diff --git a/src/component/Header.jsx b/src/component/Header.jsx new file mode 100644 index 00000000..46bf9d3c --- /dev/null +++ b/src/component/Header.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styled from "styled-components"; + +const HeaderContainer = styled.text` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +`; + +const Header = () => { + return ( + +

TodoMatic

+
+ ); + }; + + export default Header + \ No newline at end of file diff --git a/src/component/Input.jsx b/src/component/Input.jsx deleted file mode 100644 index 0592b378..00000000 --- a/src/component/Input.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -const Input = () => { - return ( -
- - -
- ) -} - -export default Input diff --git a/src/component/LoginPage.jsx b/src/component/LoginPage.jsx new file mode 100644 index 00000000..35ed654f --- /dev/null +++ b/src/component/LoginPage.jsx @@ -0,0 +1,97 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; + +function LoginPage() { + const navigate = useNavigate(); + + return ( +
+

๋กœ๊ทธ์ธ

+ +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
navigate("/signup")}> + ํšŒ์›๊ฐ€์ž… +
+
+ ); +} + +const styles = { + container: { + backgroundColor: "#f0f0f0", + height: "100vh", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + fontFamily: "sans-serif", + }, + title: { + fontSize: "2rem", + fontWeight: "700", + marginBottom: "40px", + }, + formWrapper: { + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: "20px", + }, + inputContainer: { + display: "flex", + flexDirection: "column", + gap: "10px", + }, + inputRow: { + display: "flex", + alignItems: "center", + }, + label: { + width: "80px", + fontSize: "1rem", + fontWeight: "600", + textAlign: "right", + marginRight: "10px", + }, + input: { + width: "200px", + padding: "8px", + fontSize: "1rem", + borderRadius: "5px", + border: "1px solid #ccc", + }, + loginButton: { + backgroundColor: "#444", + color: "#fff", + padding: "30px 20px", + border: "none", + borderRadius: "6px", + fontSize: "1rem", + fontWeight: "600", + cursor: "pointer", + }, + signupText: { + marginTop: "30px", + color: "#333", + fontSize: "1rem", + fontWeight: "600", + cursor: "pointer", + }, +}; + +export default LoginPage; diff --git a/src/component/SignupPage.jsx b/src/component/SignupPage.jsx new file mode 100644 index 00000000..e00c348d --- /dev/null +++ b/src/component/SignupPage.jsx @@ -0,0 +1,94 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; + +function SignupPage() { + const navigate = useNavigate(); + + return ( +
+

ํšŒ์›๊ฐ€์ž…

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ ); +} + +const styles = { + container: { + backgroundColor: "#f0f0f0", + height: "100vh", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + fontFamily: "sans-serif", + }, + title: { + fontSize: "2rem", + fontWeight: "700", + marginBottom: "40px", + }, + formWrapper: { + display: "flex", + flexDirection: "column", // ์ˆ˜์ •๋œ ๋ถ€๋ถ„ + alignItems: "center", + gap: "20px", + }, + inputContainer: { + display: "flex", + flexDirection: "column", + gap: "15px", + }, + inputRow: { + display: "flex", + alignItems: "center", + }, + label: { + width: "80px", + fontSize: "1rem", + fontWeight: "600", + textAlign: "right", + marginRight: "10px", + }, + input: { + width: "200px", + padding: "8px", + fontSize: "1rem", + borderRadius: "5px", + border: "1px solid #ccc", + }, + signupButton: { + backgroundColor: "#444", + color: "#fff", + padding: "15px 20px", + border: "none", + borderRadius: "6px", + fontSize: "1.5rem", + fontWeight: "400", + cursor: "pointer", + marginTop: "20px", + width: "150px", + alignSelf: "center", + }, +}; + +export default SignupPage; diff --git a/src/component/Text.jsx b/src/component/Text.jsx deleted file mode 100644 index 6fe9ec78..00000000 --- a/src/component/Text.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' - -const Text = () => { - return ( -
-

TodoMatic

-

What needs to be done?

-
- ) -} - -export default Text diff --git a/src/component/TodoList.jsx b/src/component/TodoList.jsx new file mode 100644 index 00000000..3daff4c4 --- /dev/null +++ b/src/component/TodoList.jsx @@ -0,0 +1,198 @@ +import styled from "styled-components"; +import React, { useState } from 'react'; + +const TodoListContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +`; + +const StyledText = styled.h2` + align-self: flex-start; + margin-left: 200px; +`; + +const StyledCheckbox = styled.input.attrs({ type: 'checkbox' })` + appearance: none; + width: 40px; + height: 40px; + margin-right: 10px; + border: 2px solid black; + background-color: white; + position: relative; + margin-right: 10px; + + &:checked::after { + content: ''; + position: absolute; + top: 5px; + left: 12px; + width: 8px; + height: 16px; + border: solid black; + border-width: 0 4px 4px 0; + transform: rotate(45deg); + }; +`; + +const StyledButton = styled.button` + padding: 7px 80px; + font-size: 14px; + border: none; + border-radius: 0px; + margin: 5px; +`; + +const EditButton = styled(StyledButton)` + background-color: white; + border: 1px solid black; +`; + +const DeleteButton = styled(StyledButton)` + background-color: #d9534f; + color: white; +`; + +const SaveButton = styled(StyledButton)` + background-color: black; + color: white; + `; + +const CancelButton = styled(StyledButton)` + background-color: white; + border: 1px, solid, black; + color: black; + `; + +const TaskItem = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 20px; +`; + +const TaskTop = styled.div` + display: flex; + align-items: center; +`; + +const TaskActions = styled.div` + display: flex; + margin-top: 5px; +`; + +const TodoList = ({ todos, setTodos, activeCategory }) => { + const [editTexts, setEditTexts] = useState({}); + + const handleDelete = (id) => { + setTodos(todos.filter(todo => todo.id !== id)); + }; + + const handleToggleComplete = (id) => { + setTodos( + todos.map(todo => + todo.id === id ? { ...todo, completed: !todo.completed } : todo + ) + ); + }; + + const handleEdit = (id, currentText) => { + setEditTexts(prev => ({...prev, [id]:currentText})); + setTodos( + todos.map(todo => + todo.id === id ? {...todo, isEditing: true}: todo + ) + ); + }; + + const handleSave =(id) => { + setTodos( + todos.map(todo => + todo.id === id + ? { ...todo, text: editTexts[id], isEditing: false } + : todo + ) + ); + setEditTexts(prev => { + const newEditTexts = { ...prev }; + delete newEditTexts[id]; + return newEditTexts; + }); + }; + + const handleCancel = (id) => { + setTodos( + todos.map(todo => + todo.id === id ? { ...todo, isEditing: false } : todo + ) + ); + setEditTexts(prev => { + const newEditTexts = { ...prev }; + delete newEditTexts[id]; + return newEditTexts; + }); + }; + + const handleEditChange = (id, newText) => { + setEditTexts(prev => ({ ...prev, [id]: newText })); + }; + + const filteredTodos = todos.filter(todo => { + if (activeCategory === "Active") return !todo.completed; + if (activeCategory === "Completed") return todo.completed; + return true; + }); + + return ( + + {todos.filter(todo => !todo.completed).length} tasks remaining +
+ {filteredTodos.map(todo => ( + + {todo.isEditing ? ( + <> + +
+ New name for {todo.text} +
+ + handleEditChange(todo.id, e.target.value)} + style={{ width: "400px", padding: "8px", fontSize: "16px" }} + /> +
+ + + handleCancel(todo.id)}>Cancel + handleSave(todo.id)}>Save + + + ) : ( + <> + + handleToggleComplete(todo.id)} + /> + + {todo.text} + + + + handleEdit(todo.id, todo.text)}>Edit + handleDelete(todo.id)}>Delete + + + )} +
+ ))} +
+
+ ); +}; + +export default TodoList; + diff --git a/src/component/TodoPage.jsx b/src/component/TodoPage.jsx new file mode 100644 index 00000000..b0e23b77 --- /dev/null +++ b/src/component/TodoPage.jsx @@ -0,0 +1,62 @@ +import styled from "styled-components"; +import Header from "./Header"; +import AddTodo from "./AddTodo"; +import Category from "./Category"; +import TodoList from "./TodoList"; +import { useState } from "react"; +import { useEffect } from "react"; + +const Container = styled.div` + max-width: 800px; + margin: 0 auto; + padding: 20px; + background-color: #f9f9f9; + + @media (max-width: 768px) and (orientation: portrait) { + max-width: 90%; + padding: 1.5rem; + } + + @media (max-width: 480px) and (orientation: portrait) { + padding: 1rem; + } +`; + +function TodoPage() { + const [todos, setTodos] = useState([]); + const [isLoaded, setIsLoaded] = useState(false); + const [activeCategory, setActiveCategory] = useState("All"); + + useEffect(() => { + const savedTodos = localStorage.getItem("tasks"); + if (savedTodos) { + setTodos(JSON.parse(savedTodos)); + } + setIsLoaded(true); + }, []); + + useEffect(() =>{ + if (isLoaded) { + localStorage.setItem("tasks", JSON.stringify(todos)); + } + }, [todos, isLoaded]); + + const handleAddTodo = (text) => { + if (text.trim() === "") return; + const newTodo = { id: Date.now(), text, completed: false, isEditing: false }; + setTodos([...todos, newTodo]); + }; + + if (!isLoaded) return
Loading...
; + + return ( + +
+ + + + + ); +}; + +export default TodoPage;