diff --git a/App.js b/App.js new file mode 100644 index 0000000..ac98622 --- /dev/null +++ b/App.js @@ -0,0 +1,26 @@ +import {useState}from 'react'; + +function App() { + const [time, setTime] = useState(1); + const handleClick = () => { + let newTime; + if (time >= 22){ + newTime =1; + }else { + newTime = time +1; + } + setTime(newTime); + }; + + console.log(' 업데이트'); + + + return ( +
+ 현재 시각 : {time}시 + +
+ ); +} + +export default App; \ No newline at end of file diff --git a/bg-desktop-dark.jpg b/bg-desktop-dark.jpg new file mode 100644 index 0000000..394ebb9 Binary files /dev/null and b/bg-desktop-dark.jpg differ diff --git a/bg-desktop-light.jpg b/bg-desktop-light.jpg new file mode 100644 index 0000000..1b5f3bb Binary files /dev/null and b/bg-desktop-light.jpg differ diff --git a/bg-mobile-dark.jpg b/bg-mobile-dark.jpg new file mode 100644 index 0000000..3285a35 Binary files /dev/null and b/bg-mobile-dark.jpg differ diff --git a/bg-mobile-light.jpg b/bg-mobile-light.jpg new file mode 100644 index 0000000..9df5c53 Binary files /dev/null and b/bg-mobile-light.jpg differ diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000..1e2df7f Binary files /dev/null and b/favicon-32x32.png differ diff --git a/html/.vscode/Oval.png b/html/.vscode/Oval.png new file mode 100644 index 0000000..f9266fb Binary files /dev/null and b/html/.vscode/Oval.png differ diff --git a/html/.vscode/Oval2.png b/html/.vscode/Oval2.png new file mode 100644 index 0000000..bdb9980 Binary files /dev/null and b/html/.vscode/Oval2.png differ diff --git a/html/.vscode/Oval3.png b/html/.vscode/Oval3.png new file mode 100644 index 0000000..061e86b Binary files /dev/null and b/html/.vscode/Oval3.png differ diff --git a/html/.vscode/assets/Oval.png b/html/.vscode/assets/Oval.png new file mode 100644 index 0000000..f9266fb Binary files /dev/null and b/html/.vscode/assets/Oval.png differ diff --git a/html/.vscode/assets/Oval2.png b/html/.vscode/assets/Oval2.png new file mode 100644 index 0000000..bdb9980 Binary files /dev/null and b/html/.vscode/assets/Oval2.png differ diff --git a/html/.vscode/assets/Oval3.png b/html/.vscode/assets/Oval3.png new file mode 100644 index 0000000..061e86b Binary files /dev/null and b/html/.vscode/assets/Oval3.png differ diff --git a/html/.vscode/assets/dark.jpg b/html/.vscode/assets/dark.jpg new file mode 100644 index 0000000..394ebb9 Binary files /dev/null and b/html/.vscode/assets/dark.jpg differ diff --git a/html/.vscode/assets/light.jpg b/html/.vscode/assets/light.jpg new file mode 100644 index 0000000..1b5f3bb Binary files /dev/null and b/html/.vscode/assets/light.jpg differ diff --git a/html/.vscode/assets/stars.png b/html/.vscode/assets/stars.png new file mode 100644 index 0000000..85bdfd0 Binary files /dev/null and b/html/.vscode/assets/stars.png differ diff --git a/html/.vscode/assets/top-image.png b/html/.vscode/assets/top-image.png new file mode 100644 index 0000000..07a03a8 Binary files /dev/null and b/html/.vscode/assets/top-image.png differ diff --git a/html/.vscode/layout.css b/html/.vscode/layout.css new file mode 100644 index 0000000..dc3b110 --- /dev/null +++ b/html/.vscode/layout.css @@ -0,0 +1,244 @@ +.background { + display: flex; + width: 100vw; + height: 100vh; + background: #fff; + position: relative; + justify-content: center; + align-items: center; + flex-direction: column; +} + +body { + font-family: 'League Spartan', sans-serif; + margin: 0; /* 기본 여백 제거 */ +} + +.path { + position: absolute; + bottom: 0; + right: 0; + max-width: 80vw; /* 최대 너비를 화면의 80%로 제한 */ + max-height: 80vh; /* 최대 높이를 화면의 80%로 제한 */ + width: auto; /* 자동 너비 조정 */ + height: auto; /* 자동 높이 조정 */ +} + +.path2 { + position: absolute; + top: 0; + left: 0; + max-width: 80vw; /* 최대 너비를 화면의 80%로 제한 */ + max-height: 80vh; /* 최대 높이를 화면의 80%로 제한 */ + width: auto; /* 자동 너비 조정 */ + height: auto; /* 자동 높이 조정 */ +} + +.title-container { + display: flex; + flex-direction: column; + position: absolute; + top: 50px; + left: 165px; + width: 445px; + color: #512051; + margin-bottom: 20px; +} + +.title { + font-size: 61px; + font-weight: 800; + line-height: 48px; + letter-spacing: -2px; + margin-bottom: 15px; +} + +.subtitle { + color: #927B91; + font-size: 24px; + font-weight: 500; + margin-bottom: 30px; + line-height: 25px; +} + +.container2 { + display: flex; + flex-direction: column; + position: absolute; + top: 118px; + left: 700px; + width: 445px; + gap: 20px; + color: #512051; +} + +.box_s { + display: flex; + align-items: center; + background: #F7F2F7; + border-radius: 8px; + width: 100%; + height: 56px; + gap: 32.45px; +} + +.star { + margin-left: 32px; + height: auto; +} + +.container { + display: flex; + flex-direction: row; + position: absolute; + top: 434px; + align-items: center; + width: calc(100% - 330px); + gap: 20px; +} + +.box_B { + display: flex; + flex-direction: column; + width: 350px; + height: 225px; + background: #512051; + border-radius: 8px; + padding: 20px; + color: white; +} +.box_B_content { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.img { + width: 50px; + height: 50px; + margin-left: 32PX; + border-radius: 50%; + margin-top: 30px; + margin-right: 23px; +} + +.b_title_subtitle { + display: flex; + flex-direction: column; + margin-top: 30px; + +} +.b_title { + font-size: 17px; + font-weight: 700; + margin-bottom: 4px; +} + +.bolder { + font-weight: bold; +} + +.b_subtitle { + font-size: 17px; + color: #EE69A4; + font-weight: 400; +} + +.b_text { + margin : 22px; + width: 85%; + font-size: 16.5px; + font-weight: 500; + line-height: 22px; + +} + +/* 미디어 쿼리 추가로 반응형 디자인 설정 */ +@media (max-width: 375px) { + body { + + margin: 0; /* 기본 여백 제거 */ + } + .title-container { + align-items: center; + text-align: center; + left: 50%; + transform: translateX(-50%); + } + .title{ + font-size: 60px; + } + .subtitle { + margin-bottom: 39px; + font-size: 19px; + width : 335px; + } + .path { + top: 1401.396; + } + + .container2 { + position: relative; + top: 10px; + left: auto; + width: 85%; + height : 25%; + margin-top: 337px; + margin-bottom: 237px; + gap: 20px; + align-items: center; + + } + .container2 span{ + margin-top : -20px; + } + + .star { + margin-top : 16px; + margin-right : 16px; + + } + + .container { + flex-direction: column; + align-content: center; + margin-top: 260px; + width: 78%; + height : 25%; + + } + .box_s{ + display : flex; + flex-direction: column; + align-items: center; + height : 95px; + } + + .box_B { + height: 23vh; + width: 77vw; + margin-bottom: 0px; + align-content: center; + padding-left: 12px; + padding-right: 12px; + + } + .img{ + margin-top : 15px; + margin-left : 10px; + } + .b_title_subtitle{ + margin-top : 15px; + } + .b_title{ + margin-bottom: 8px; + } + .b_text{ + margin-top : 10px; + font-size: 16px; + width : 93%; + align-items: center; + margin-left : 10px; + } +} + diff --git a/html/.vscode/layout.html b/html/.vscode/layout.html new file mode 100644 index 0000000..38dbba7 --- /dev/null +++ b/html/.vscode/layout.html @@ -0,0 +1,91 @@ + + + + + + + layout + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+

10,000+ of our users love our products.

+

We only provide great products combined with excellent customer service. See what our satisfied customers are saying about our services.

+
+ +
+
+ + Rated 5 Stars in Reviews +
+
+ + Rated 5 Stars in Report Guru +
+
+ + Rated 5 Stars in BestTech +
+
+ +
+
+
+ +
+
Colton Smith
+
Verified Buyer
+
+
+
“We needed the same printed design as the one we had ordered a week prior. Not only did they find the original order, but we also received it in time. Excellent!”
+
+
+
+ +
+
Irene Roberts
+
Verified Buyer
+
+
+
“Customer service is always excellent and very quick turn around. Completely delighted with the simplicity of the purchase and the speed of delivery.”
+
+
+
+ +
+
Anne Wallace
+
Verified Buyer
+
+
+
“Put an order with this company and can only praise them for the very high standard. Will definitely use them again and recommend to everyone!”
+
+
+
+ + diff --git a/html/.vscode/recipe.css b/html/.vscode/recipe.css new file mode 100644 index 0000000..9b68a9f --- /dev/null +++ b/html/.vscode/recipe.css @@ -0,0 +1,133 @@ +.container { + display: flex; + max-width: 1440px; + padding: var(--spacing-1600, 128px) var(--spacing-600, 48px); + justify-content: center; + align-items: center; + gap: 8px; + background: #F3E5D7; + margin: auto; +} +.white_box { + display: flex; + width: 736px; + padding: 40px; + flex-direction: column; + align-items: flex-start; + gap: 10px; + flex-shrink: 0; + border-radius: 24px; + background: #FFF; +} +img { + height: 300px; + align-self: stretch; + border-radius: 12px; +} +.headtitle { + color: #312E2C; + font-family: 'Young Serif', serif; + font-size: 40px; + line-height: 100%; + align-self: stretch; + padding-top: 40px; + padding-bottom: 20px; +} +.subtitle { + color: #5F564D; + font-family: 'Outfit', sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 150%; + align-self: stretch; + padding-bottom: 30px; +} +.pink_box { + font-family: 'Young Serif', serif; + display: flex; + padding: 24px; + flex-direction: column; + align-items: flex-start; + border-radius: 12px; + background: #FFF7FB; + width: 92%; + height: 150px; +} +.title_pink { + align-self: stretch; + color: #7A284E; + font-family: 'Outfit', sans-serif; + font-size: 20px; + font-weight: 600; + line-height: 100%; +} +.Red_title { + align-self: stretch; + color: #854632; + font-family: 'Young Serif', serif; + font-size: 28px; + line-height: 100%; + padding-top: 30px; + padding-bottom: 10px; +} +.content { + line-height: 150%; + font-size: var(--font-size-desktop-text-preset-4, 18px); + color: #5F564D; + margin-bottom: 16px; + font-family: 'Outfit', sans-serif; + font-weight: normal; +} +.nutrition { + display: flex; + justify-content: space-between; + align-items: center; + color: #5F564D; + font-family: 'Outfit', sans-serif; + font-size: 16px; + font-weight: 400; + width: 100%; + line-height: 150%; +} +.nutrition-info { + flex: 1; + text-align: left; + margin-left: 5%; + padding-right: 20px; +} +.nutrition-item { + color: #854632; + font-family: 'Outfit', sans-serif; + font-weight: 700; + text-align: left; + margin-left: auto; + margin-right: 45%; + min-width: 60px; +} +.bolder { + font-weight: bolder; + color: #5F564D; + margin-bottom: 10px; +} +.line { + width: 720px; + height: 1px; + align-self: stretch; + background: #D8D3CD; + margin: 8px 0px; +} +.bullet { + display: flex; + align-items: center; + gap: 8px; + line-height: 150%; + margin-left: -30px; +} +.bullet-icon { + width: 4px; + height: 4px; + flex-shrink: 0; + border-radius: 50%; + background: #854632; + margin-top: -14px; +} diff --git a/html/.vscode/recipe.html b/html/.vscode/recipe.html new file mode 100644 index 0000000..c5cd3bb --- /dev/null +++ b/html/.vscode/recipe.html @@ -0,0 +1,107 @@ + + + + + + Recipe + + + + +
+
+ Top Image +
Simple Omelette Recipe
+
An easy and quick dish, perfect for any meal. This classic omelette combines beaten eggs cooked to perfection, optionally filled with your choice of cheese, vegetables, or meats.
+
+
Preparation time
+
    +
  • +
    +
    Total: Approximately 10 minutes
    +
  • +
  • +
    +
    Preparation: 5 minutes
    +
  • +
  • +
    +
    Cooking: 5 minutes
    +
  • +
+
+
Ingredients
+ +
+
Instructions
+
+
    +
  1. + 1. + Beat the eggs: In a bowl, beat the eggs with a pinch of salt and pepper until they are well mixed. You can add a tablespoon of water or milk for a fluffier texture. +
  2. +
  3. + 2. + Heat the pan: Place a non-stick frying pan over medium heat and add butter or oil. +
  4. +
  5. + 3. + Cook the omelette: Once the butter is melted and bubbling, pour in the eggs. Tilt the pan to ensure the eggs evenly coat the surface. +
  6. +
  7. + 4. + Add fillings (optional): When the eggs begin to set at the edges but are still slightly runny in the middle, sprinkle your chosen fillings over one half of the omelette. +
  8. +
  9. + 5. + Fold and serve: Carefully fold the omelette in half and cook for an additional minute. Slide it onto a plate and serve warm. +
  10. +
+
+
+
Nutrition
+
The table below shows nutritional values per serving without the additional fillings.
+
+
Calories
+
277kcal
+
+
+
+
Carbs
+
1g
+
+
+
+
Proteins
+
18g
+
+
+
+
Fats
+
22g
+
+
+
+ + diff --git a/html/.vscode/stars.png b/html/.vscode/stars.png new file mode 100644 index 0000000..85bdfd0 Binary files /dev/null and b/html/.vscode/stars.png differ diff --git a/html/.vscode/todo.css b/html/.vscode/todo.css new file mode 100644 index 0000000..19d7e2e --- /dev/null +++ b/html/.vscode/todo.css @@ -0,0 +1,277 @@ +/* General Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Outfit', sans-serif; +} + +body { + min-height: 100vh; /* 화면의 최소 높이를 설정하여 스크롤이 가능하게 함 */ + background: #FAFAFA; + display: flex; + flex-direction: column; /* 세로로 정렬 */ + justify-content: flex-start; /* 내용이 화면 위에 정렬되도록 */ + align-items: center; + overflow: auto; /* 전체 페이지가 스크롤 되도록 설정 */ +} + +/* 배경 이미지 */ +.background-image { + position: absolute; + top: 0; + background: url('assets/light.jpg') lightgray no-repeat; + background-position: center; + flex-shrink: 0; + z-index: -1; +} + +/* task */ +.todo-app { + width: 540px; + flex-shrink: 0; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + margin-top: 24px; + margin-bottom : 49px; + padding-top: 0; + display: flex; + flex-direction: column; + height: auto; /* 내용에 맞게 높이를 자동으로 조정 */ + max-height: calc(100vh - 80px); /* 최대 높이를 화면 높이에 맞게 설정 */ + overflow-y: auto; /* 내용이 넘칠 경우 스크롤 가능 */ +} + +.container { + margin: 0; + margin-top: 50px; + padding: 0; + width: 540px; + justify-content: space-between; +} + +/* 제목과 테마 토글을 같은 줄에 배치하기 위해 .title-container 생성 */ +.title-container { + flex-direction: row; + width: 541px; + height: 48px; + flex-shrink: 0; + justify-content: space-between; + align-items: center; + padding-top: 0; /* 패딩 제거 */ + display: flex; + flex-shrink: 0; + margin-bottom: 40px; +} + +.title { + color: #FFF; + font-family: "Josefin Sans"; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: 15px; +} + +/* theme toggle 버튼을 title의 오른쪽에 24px 간격으로 배치 */ +.theme-toggle { + width: 25.113px; + height: 26px; + flex-shrink: 0; + top : 11px; + fill: #FFF; +} + +#new-task { + width: 100%; + padding: 1rem; + border: 1px solid hsl(30, 54%, 90%); + border-radius: 8px; + margin-top: 1rem; + color: #393A4B; + font-family: "Josefin Sans"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; +} + +/* Container for the tasks */ +#taskList { + display: flex; + flex-direction: column; + gap: 10px; /* Adjust the space between tasks */ + padding: 10px; + max-height: none; /* Ensure that the height expands based on content */ + overflow: visible; /* Allow the container to grow */ + margin-top: 1rem; /* task 항목 위에 여백 추가 */ +} + +.task { + display: flex; + align-items: center; + padding: 0.75rem; + border-bottom: 1px solid #E3E4F1; + font-size: 18px; +} + +.task-completed { + text-decoration: line-through; + color: #D1D2DA; + font-family: "Josefin Sans"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; +} + +.task-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-left : 24px; + margin-bottom : 20px; + margin-right : 24px; + margin-top: 16px; +} + +#remaining-tasks { + color: #9495A5; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + +.filter-btn { + color: #9495A5; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; + background: none; + border: none; + cursor: pointer; +} + +.filter-btn.active { + color: #3A7CFD; + font-weight: bold; +} + +.filter-btn:hover { + color: #3A7CFD; +} + +#clear-completed { + color: #9495A5; + background: none; + border: none; + cursor: pointer; +} + +#clear-completed:hover { + color: #3A7CFD; +} + +.filter { + width : 166px; + justify-content: space-between; +} + +/* 하단 텍스트 */ +.footer { + font-size: 14px; + color: #999; + text-align: center; + margin-top: 24px; + padding: 20px; + background-color: #FAFAFA; + width: 100%; +} + +/* Dark Mode */ + +/* 전체 배경 */ +body.dark-mode { + background: #171823; +} + +body.dark-mode .background-image { + background: url('assets/dark.jpg') lightgray no-repeat; +} + +/* create a new todo .. box */ +body.dark-mode #new-task { + border-radius: 5px; + background: #25273D; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); + border:0px; + color: #C8CBE7; +} + +/* task 박스 */ +body.dark-mode .todo-app { + border-radius: 5px; + background: #25273D; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); +} + +body.dark-mode .task { + color: #C8CBE7; + font-family: "Josefin Sans"; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; + border-bottom: 1px solid #393A4B; +} + +/* Hover Effects */ +.task:hover .delete-icon { + display: inline; + width: 18px; + height: 18px; + flex-shrink: 0; +} + +/* 삭제 아이콘 숨기기.hover 안되었을 때 */ +.delete-icon { + display: none; + cursor: pointer; + margin-left: auto; +} + +input { + margin-right: 24px; +} + +input:hover { + flex-shrink: 0; +} + +.custom-checkbox { + appearance: none; + width: 24px; + height: 24px; + background-image: url('assets/circle1.png'); /* 기본 이미지 */ + background-size: cover; + cursor: pointer; +} + +.custom-checkbox:hover { + background-image: url('assets/circle2.png'); /* 호버 이미지 */ +} + +.custom-checkbox:checked { + background-image: url('assets/circle3.png'); /* 클릭(체크됨) 이미지 */ +} diff --git a/html/.vscode/todo.html b/html/.vscode/todo.html new file mode 100644 index 0000000..5f505be --- /dev/null +++ b/html/.vscode/todo.html @@ -0,0 +1,58 @@ + + + + + + Todo App + + + + + +
+
+
+

TODO

+
+ + + + + + + +
+
+ +
+ +
+ 0 items left +
+ + + +
+ +
+
+
+ + + + + diff --git a/html/.vscode/todo.js b/html/.vscode/todo.js new file mode 100644 index 0000000..ecc4830 --- /dev/null +++ b/html/.vscode/todo.js @@ -0,0 +1,133 @@ +document.addEventListener('DOMContentLoaded', () => { + const newTaskInput = document.getElementById('new-task'); + const taskList = document.getElementById('task-list'); + const remainingTasks = document.getElementById('remaining-tasks'); + const filterButtons = document.querySelectorAll('.filter-btn'); + const clearCompletedButton = document.getElementById('clear-completed'); + const themeToggle = document.querySelector('.theme-toggle'); + const lightModeIcon = document.querySelector('.light-mode-icon'); + const darkModeIcon = document.querySelector('.dark-mode-icon'); + + let tasks = JSON.parse(localStorage.getItem('tasks')) || []; + let darkMode = JSON.parse(localStorage.getItem('darkMode')) || false; + + const updateTaskList = () => { + taskList.innerHTML = ''; + tasks.forEach((task, index) => { + const taskItem = document.createElement('li'); + taskItem.classList.add('task'); + taskItem.innerHTML = ` + + ${task.text} + + + +`; + taskList.appendChild(taskItem); + + const checkbox = taskItem.querySelector('input[type="checkbox"]'); + const taskText = taskItem.querySelector('span'); + checkbox.addEventListener('click', () => toggleTaskCompletion(index)); + taskText.addEventListener('click', () => toggleTaskCompletion(index)); + + const deleteIcon = taskItem.querySelector('.delete-icon'); + deleteIcon.addEventListener('click', () => deleteTask(index)); + }); + + const remainingCount = tasks.filter(task => !task.completed).length; + remainingTasks.textContent = `${remainingCount} item${remainingCount !== 1 ? 's' : ''} left`; + localStorage.setItem('tasks', JSON.stringify(tasks)); + }; + + const toggleTaskCompletion = (index) => { + tasks[index].completed = !tasks[index].completed; + updateTaskList(); + }; + + const deleteTask = (index) => { + tasks.splice(index, 1); + updateTaskList(); + }; + + const addTask = (e) => { + if (e.key === 'Enter' && newTaskInput.value.trim() !== '') { + tasks.push({ + text: newTaskInput.value, + completed: false + }); + newTaskInput.value = ''; + updateTaskList(); + } + }; + + const toggleTheme = () => { + darkMode = !darkMode; + localStorage.setItem('darkMode', JSON.stringify(darkMode)); + document.body.classList.toggle('dark-mode', darkMode); + + if (darkMode) { + lightModeIcon.style.display = 'none'; + darkModeIcon.style.display = 'inline'; + } else { + lightModeIcon.style.display = 'inline'; + darkModeIcon.style.display = 'none'; + } + }; + + const filterTasks = (filter) => { + filterButtons.forEach(btn => btn.classList.remove('active')); + const activeFilterButton = document.getElementById(`filter-${filter}`); + activeFilterButton.classList.add('active'); + + const filteredTasks = tasks.filter(task => { + if (filter === 'all') return true; + if (filter === 'active') return !task.completed; + if (filter === 'completed') return task.completed; + }); + + taskList.innerHTML = ''; + filteredTasks.forEach((task, index) => { + const taskItem = document.createElement('li'); + taskItem.classList.add('task'); + taskItem.innerHTML = ` + + ${task.text} + + + + `; + taskList.appendChild(taskItem); + + const checkbox = taskItem.querySelector('input[type="checkbox"]'); + const taskText = taskItem.querySelector('span'); + checkbox.addEventListener('click', () => toggleTaskCompletion(index)); + taskText.addEventListener('click', () => toggleTaskCompletion(index)); + + const deleteIcon = taskItem.querySelector('.delete-icon'); + deleteIcon.addEventListener('click', () => deleteTask(index)); + }); + }; + + const clearCompleted = () => { + tasks = tasks.filter(task => !task.completed); + updateTaskList(); + }; + + newTaskInput.addEventListener('keydown', addTask); + clearCompletedButton.addEventListener('click', clearCompleted); + themeToggle.addEventListener('click', toggleTheme); + document.getElementById('filter-all').addEventListener('click', () => filterTasks('all')); + document.getElementById('filter-active').addEventListener('click', () => filterTasks('active')); + document.getElementById('filter-completed').addEventListener('click', () => filterTasks('completed')); + + if (darkMode) { + document.body.classList.add('dark-mode'); + lightModeIcon.style.display = 'none'; + darkModeIcon.style.display = 'inline'; + } else { + lightModeIcon.style.display = 'inline'; + darkModeIcon.style.display = 'none'; + } + + updateTaskList(); +}); diff --git a/html/.vscode/todo/images/bg-desktop-dark.jpg b/html/.vscode/todo/images/bg-desktop-dark.jpg new file mode 100644 index 0000000..394ebb9 Binary files /dev/null and b/html/.vscode/todo/images/bg-desktop-dark.jpg differ diff --git a/html/.vscode/todo/images/bg-desktop-light.jpg b/html/.vscode/todo/images/bg-desktop-light.jpg new file mode 100644 index 0000000..1b5f3bb Binary files /dev/null and b/html/.vscode/todo/images/bg-desktop-light.jpg differ diff --git a/html/.vscode/todo/images/bg-mobile-dark.jpg b/html/.vscode/todo/images/bg-mobile-dark.jpg new file mode 100644 index 0000000..3285a35 Binary files /dev/null and b/html/.vscode/todo/images/bg-mobile-dark.jpg differ diff --git a/html/.vscode/todo/images/bg-mobile-light.jpg b/html/.vscode/todo/images/bg-mobile-light.jpg new file mode 100644 index 0000000..9df5c53 Binary files /dev/null and b/html/.vscode/todo/images/bg-mobile-light.jpg differ diff --git a/html/.vscode/todo/images/favicon-32x32.png b/html/.vscode/todo/images/favicon-32x32.png new file mode 100644 index 0000000..1e2df7f Binary files /dev/null and b/html/.vscode/todo/images/favicon-32x32.png differ diff --git a/html/.vscode/todo/images/icon-check.svg b/html/.vscode/todo/images/icon-check.svg new file mode 100644 index 0000000..61e7384 --- /dev/null +++ b/html/.vscode/todo/images/icon-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/html/.vscode/todo/images/icon-cross.svg b/html/.vscode/todo/images/icon-cross.svg new file mode 100644 index 0000000..cdf9c7c --- /dev/null +++ b/html/.vscode/todo/images/icon-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/html/.vscode/todo/images/icon-moon.svg b/html/.vscode/todo/images/icon-moon.svg new file mode 100644 index 0000000..60c2ace --- /dev/null +++ b/html/.vscode/todo/images/icon-moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/html/.vscode/todo/images/icon-sun.svg b/html/.vscode/todo/images/icon-sun.svg new file mode 100644 index 0000000..24f69f3 --- /dev/null +++ b/html/.vscode/todo/images/icon-sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/html/.vscode/todo/index.html b/html/.vscode/todo/index.html new file mode 100644 index 0000000..f739204 --- /dev/null +++ b/html/.vscode/todo/index.html @@ -0,0 +1,48 @@ + + + + + + + Frontend Mentor | Todo app + + + + + + +
+ +
+
TODO
+ +
+ +
+
+
+ +
+
+ +
+
+
0 items left
+
+ + + +
+ +
+ + + + + diff --git a/html/.vscode/todo/script.js b/html/.vscode/todo/script.js new file mode 100644 index 0000000..192cfba --- /dev/null +++ b/html/.vscode/todo/script.js @@ -0,0 +1,417 @@ +// 주요 DOM 요소 가져오기 +const themeToggle = document.querySelector('.toggle'); // 테마 전환 버튼 +const themeIcon = document.getElementById('icon'); // 테마 아이콘 +const background = document.getElementById('background-image'); // 배경 이미지 +const newTodoInput = document.getElementById('input-text'); // 새로운 할 일 입력 필드 +const mainContainer = document.querySelector('.main-container'); // 할 일 목록이 표시될 메인 컨테이너 +const itemsLeft = document.querySelector('.remain-item'); // 남은 할 일 수 표시 +const clearCompletedButton = document.querySelector('.clear'); // 완료된 할 일 삭제 버튼 +const filterButtons = document.querySelectorAll('.filter-button'); // 필터 버튼들 +const addTodoOval = document.querySelector('.oval'); // 할 일 추가 버튼의 Oval +const inputContainer = document.querySelector('.input-container'); // 입력 컨테이너 +const bottomContainer = document.querySelector('.bottom-container'); // 하단 컨테이너 + + +// let todos = []; +let todos = JSON.parse(localStorage.getItem("todos")) || []; +let draggedIndex = null; + +// Oval 상태 로컬 스토리지에서 가져오기 +let ovalState = localStorage.getItem('ovalState') || 'light'; + +// 기본, hover, 완료 상태 SVG +const lightOvalSVG = ` + + + `; + +const darkOvalSVG = ` + + + `; + +const clickedOval = ` + + + + + + + + + + + `; + +const hoverOval = ` + + + + + + + + + + + + + + + + + `; + + +// Oval 업데이트 함수 +function updateAddTodoOval() { + const isDark = document.body.classList.contains('dark'); + addTodoOval.innerHTML = isDark ? darkOvalSVG : lightOvalSVG; +} + +// 테마 전환 함수에서 line과 텍스트 색상 업데이트 추가 +const toggleTheme = () => { + document.body.classList.toggle('dark'); + const isDark = document.body.classList.contains('dark'); + + // 아이콘과 배경 이미지 변경 + themeIcon.src = isDark ? 'images/icon-sun.svg' : 'images/icon-moon.svg'; + background.style.backgroundImage = isDark + ? 'url(images/bg-desktop-dark.jpg)' + : 'url(images/bg-desktop-light.jpg)'; + + // 낮/밤 모드에 따른 배경색 변경 + document.body.style.background = isDark ? '#171823' : '#FAFAFA'; + + // 주요 컨테이너 배경색 + const containerBgColor = isDark ? '#25273D' : '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + + // line 및 텍스트 색상 업데이트 + document.querySelectorAll('.line').forEach(line => { + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + }); + document.querySelectorAll('.todo-text').forEach(text => { + text.style.color = isDark ? '#C8CBE7' : '#494C6B'; + }); + + // 로컬 스토리지에 테마 상태 저장 + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + + // Oval 상태 업데이트 + ovalState = isDark ? 'dark' : 'light'; + localStorage.setItem('ovalState', ovalState); + updateAddTodoOval(); + updateTodoList(); +}; + +// 초기 로딩 시 테마 및 할 일 설정 +document.addEventListener('DOMContentLoaded', function () { + const savedTheme = localStorage.getItem('theme'); + const isDark = savedTheme === 'dark'; + + if (isDark) { + document.body.classList.add('dark'); + themeIcon.src = 'images/icon-sun.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-dark.jpg)'; + document.body.style.background = '#171823'; + const containerBgColor = '#25273D'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + } else { + document.body.classList.remove('dark'); + themeIcon.src = 'images/icon-moon.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-light.jpg)'; + document.body.style.background = '#FAFAFA'; + const containerBgColor = '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + + } + + updateAddTodoOval(); + + const allButton = document.getElementById('all'); + allButton.classList.add('active'); + filterTodos('all'); + + const savedTodos = JSON.parse(localStorage.getItem('todos')); + if (savedTodos) { + todos = savedTodos; + updateAddTodoOval(); + updateTodoList(); + } +}); + +// 테마 토글 버튼 이벤트 +themeToggle.addEventListener('click', toggleTheme); + +// 새로운 할 일 추가 +newTodoInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && newTodoInput.value.trim()) { + addTodo(newTodoInput.value); + newTodoInput.value = ''; + } +}); + +// 할 일 추가 함수 +function addTodo(text) { + const todo = { text, completed: false }; + todos.push(todos); + updateTodoList(); + saveTodosToLocalStorage(); + localStorage.setItem('todos', JSON.stringify(todos)); +} + +function handleDragStart(event) { + draggedIndex = event.target.getAttribute('data-index'); + event.dataTransfer.effectAllowed = "move"; +} + +function handleDragOver(event) { + event.preventDefault(); // 드롭을 허용하기 위해 필요 + event.dataTransfer.dropEffect = "move"; +} + +function handleDrop(event) { + event.preventDefault(); + const targetIndex = event.target.closest('.todo-item').getAttribute('data-index'); + + // todos 배열을 드래그한 항목과 드롭한 대상의 위치에 맞게 재정렬 + if (draggedIndex !== null && targetIndex !== null && draggedIndex !== targetIndex) { + const movedItem = todos.splice(draggedIndex, 1)[0]; + todos.splice(targetIndex, 0, movedItem); + + updateTodoList(); + saveTodosToLocalStorage(); + } + draggedIndex = null; +} + +// updateTodoList 함수에서 새로 생성되는 항목의 색상 설정 +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; + todoItem.setAttribute('data-index', index); + + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); +} + + +// 할 일 목록 업데이트 함수 (드래그 앤 드롭 통합) +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; // 드래그 가능하도록 설정 + todoItem.setAttribute('data-index', index); + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + const cancelButton = todoItem.querySelector('.cross-line'); + + // 완료된 할 일 텍스트 스타일 적용 + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + // 드래그 앤 드롭 이벤트 추가 + todoItem.addEventListener('dragstart', handleDragStart); + todoItem.addEventListener('dragover', handleDragOver); + todoItem.addEventListener('drop', handleDrop); + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + cancelButton.addEventListener('click', (e) => { + e.stopPropagation(); + deleteTodo(index); + }); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); +} + + +// 할 일 삭제 함수 +function deleteTodo(index) { + todos.splice(index, 1); + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 완료 상태 토글 함수 +function toggleComplete(index) { + todos[index].completed = !todos[index].completed; + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 남은 할 일 수 업데이트 +function updateItemsLeft() { + const remainingItems = todos.filter(todo => !todo.completed).length; + itemsLeft.textContent = `${remainingItems} items left`; +} + +// 필터 버튼 이벤트 +filterButtons.forEach(button => { + button.addEventListener('click', () => { + filterButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + filterTodos(button.dataset.filter); + }); +}); + +// 필터링 함수에서 완료된 항목 스타일 수정 +function filterTodos(filter) { + mainContainer.innerHTML = ''; + const isDark = document.body.classList.contains('dark'); + const filteredTodos = filter === 'all' ? todos : + todos.filter(todo => filter === 'active' ? !todo.completed : todo.completed); + + filteredTodos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + + `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); +} + + +// 완료된 할 일 삭제 +clearCompletedButton.addEventListener('click', () => { + todos = todos.filter(todo => !todo.completed); + updateTodoList(); + saveTodosToLocalStorage(); +}); + +// 로컬 스토리지 저장 +function saveTodosToLocalStorage() { + localStorage.setItem('todos', JSON.stringify(todos)); +} + +// 필터 버튼 스타일 설정 + +// 필터 버튼 이벤트 +filterButtons.forEach(button => { + button.addEventListener('click', () => { + filterButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + filterTodos(button.dataset.filter); + }); +}); + +function applyDarkModeShadow() { + if (document.body.classList.contains('dark')) { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + } else { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + } +} + + clear.bottom.filter-button(button => { + clear.bottom.filter-button.addEventListener('mouseover', () => { + clear.bottom.filter-button.classList.add('active'); + }); + clear.bottom.filter-button.addEventListener('mouseout', () => { + clear.bottom.filter-button.classList.remove('active'); + }); +}); \ No newline at end of file diff --git a/html/.vscode/todo/style.css b/html/.vscode/todo/style.css new file mode 100644 index 0000000..19472c2 --- /dev/null +++ b/html/.vscode/todo/style.css @@ -0,0 +1,286 @@ +.attribution { font-size: 11px; text-align: center; } + +body { + width: 100%; + background: #fafafa; + font-family: "Josefin Sans", sans-serif; + display: flex; + align-items: center; + flex-direction: column; +} + +#background-image { + position:absolute; + background-image: url("images/bg-desktop-light.jpg"); + background-position: center; + top: 0; + left: 0px; + width: 100%; + height: 300px; + z-index: -1; + background-repeat: no-repeat; + background-size: cover; +} + +.header-container{ + width: 541px; + height: 48px; + flex-shrink: 0; + margin-top: 70px; + display: flex; + justify-content: space-between; +} + +.header{ + color: #FFF; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: 15px; + margin: 0; +} + + +.input-container{ + max-width: 540px; + width: 90%; + height: 64px; + margin-top: 40px; + flex-shrink: 0; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + align-items: center; + flex-direction: row; +} + +.oval{ + width: 24px; + height: 24px; + fill: #FFF; +} + +.todo-item .oval:hover circle{ + stroke:#5df; +} + + + + +#input-text{ + color : #393A4B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; + border: none; + outline: none; +} + +.input-box{ + display: inline-flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-left: 24px; +} + +.main-container{ + max-width: 540px; + width: 90%; + min-height: 0px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + margin-top: 24px; +} + +.bottom-container{ + max-width: 540px; + width: 90%; + max-height: 439px; + min-height: 50px; + flex-shrink: 0; + display: flex; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + margin-bottom: 16px; + padding: 0 24px; + box-sizing: border-box; +} + +.toggle { + background: none; /* 배경을 없애거나 단색으로 설정 */ + border: none; /* 테두리 없애기 */ + box-shadow: none; /* 그림자 없애기 */ + padding: 0; /* 기본 여백 없애기 */ + outline: none; /* 클릭 시 외곽선 없애기 */ + cursor: pointer; /* 포인터 변경 */ +} + +.interaction-container{ + width: 116px; + height: 14px; + display: inline-flex; + padding-bottom: 4px; + justify-content: center; + align-items: flex-start; + gap: 19px; + color: #9495A5; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; + margin-left: 40px; +} + +.bottom{ + color: #9495A5; + text-align: right; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + + + +.footer{ + margin-top: 49px; + color: #9495A5; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + +.todo-item{ + display: flex; + width: 492px; + height: 24px; + flex-shrink: 0; + align-items: center; + margin-top: 20px; +} + +.todo-text{ + margin-left: 24px; + color: #494C6B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; +} + +.line{ + margin-top: 20px; + width: 540px; + height: 1px; + flex-shrink: 0; + background: #E3E4F1; +} + +.cross-line{ + margin-left: auto; + display: none; +} + +/* todo-item에 마우스를 올렸을 때만 cross-line 보이기 */ +.todo-item:hover .cross-line { + display: block; +} + + + +.filter-button{ + border: none; + outline: none; + background: none; + color: #9495A5; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; +} + +.clear{ + color: #9495A5; + text-align: right; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + +/* .filter-button:hover { + color: #E3E4F1; + text-align: right; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} */ + +.filter-button.active{ + color: #3A7CFD; + font-weight: 700; + +} +/* dark-shadow 클래스를 다크 모드 전용으로 box-shadow를 적용하는 용도로 추가 */ +.dark-shadow { + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.5); +} + + +/* Dark mode 시 main-container와 bottom-container의 box-shadow */ +body.dark .main-container, +body.dark .bottom-container { + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); +} + +/* clear bottom filter-button의 hover 스타일 */ +.clear.bottom.filter-button:hover { + color: #494C6B; + font-weight: 400; + +} +.clear.bottom.filter-button { + color: #9495A5; + font-weight: 400; + +} + +body.dark.clear.bottom.filter-button { + color: #5B5E7E; + font-weight: 400; + +} +body.dark.clear:hover { + color: #E3E4F1; + font-weight: 400; + +} \ No newline at end of file diff --git a/icon-check.svg b/icon-check.svg new file mode 100644 index 0000000..61e7384 --- /dev/null +++ b/icon-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon-cross.svg b/icon-cross.svg new file mode 100644 index 0000000..cdf9c7c --- /dev/null +++ b/icon-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon-moon.svg b/icon-moon.svg new file mode 100644 index 0000000..60c2ace --- /dev/null +++ b/icon-moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon-sun.svg b/icon-sun.svg new file mode 100644 index 0000000..24f69f3 --- /dev/null +++ b/icon-sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e52708f --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + + + + Frontend Mentor | Todo app + + + + + + + + + +
+ +
+
TODO
+ +
+ +
+
+
+ +
+
+ +
+
+ ${todo.completed ? clickedOval : (isDark ? darkOvalSVG : lightOvalSVG)} +
+ + ${todo.text} + +
+
+
0 items left
+
+ + + +
+ +
+ + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..cd64f3b --- /dev/null +++ b/script.js @@ -0,0 +1,458 @@ +// 주요 DOM 요소 가져오기 +const themeToggle = document.querySelector('.toggle'); // 테마 전환 버튼 +const themeIcon = document.getElementById('icon'); // 테마 아이콘 +const background = document.getElementById('background-image'); // 배경 이미지 +const newTodoInput = document.getElementById('input-text'); // 새로운 할 일 입력 필드 +const mainContainer = document.querySelector('.main-container'); // 할 일 목록이 표시될 메인 컨테이너 +const itemsLeft = document.querySelector('.remain-item'); // 남은 할 일 수 표시 +const clearCompletedButton = document.querySelector('.clear'); // 완료된 할 일 삭제 버튼 +const filterButtons = document.querySelectorAll('.filter-button'); // 필터 버튼들 +const addTodoOval = document.querySelector('.oval'); // 할 일 추가 버튼의 Oval +const inputContainer = document.querySelector('.input-container'); // 입력 컨테이너 +const bottomContainer = document.querySelector('.bottom-container'); // 하단 컨테이너 + + +let todos = []; + +let draggedIndex = null; + +// Oval 상태 로컬 스토리지에서 가져오기 +let ovalState = localStorage.getItem('ovalState') || 'light'; + +// 기본, hover, 완료 상태 SVG +const lightOvalSVG = ` + + + `; + +const darkOvalSVG = ` + + + `; + +const clickedOval = ` + + + + + + + + + + + `; + +const hoverOval = ` + + + + + + + + + + + + + + + + + `; + + //다크모드 그림자 +function applyDarkModeShadow() { + if (document.body.classList.contains('dark')) { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + } else { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + } +} + +// Oval 업데이트 함수 +function updateAddTodoOval() { + const isDark = document.body.classList.contains('dark'); + addTodoOval.innerHTML = isDark ? darkOvalSVG : lightOvalSVG; +} + +// 테마 전환 함수에서 line과 텍스트 색상 업데이트 추가 +const toggleTheme = () => { + document.body.classList.toggle('dark'); + const isDark = document.body.classList.contains('dark'); + + // 아이콘과 배경 이미지 변경 + themeIcon.src = isDark ? 'images/icon-sun.svg' : 'images/icon-moon.svg'; + background.style.backgroundImage = isDark + ? 'url(images/bg-desktop-dark.jpg)' + : 'url(images/bg-desktop-light.jpg)'; + + // 낮/밤 모드에 따른 배경색 변경 + document.body.style.background = isDark ? '#171823' : '#FAFAFA'; + + // 주요 컨테이너 배경색 + const containerBgColor = isDark ? '#25273D' : '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + + // line 및 텍스트 색상 업데이트 + document.querySelectorAll('.line').forEach(line => { + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + }); + document.querySelectorAll('.todo-text').forEach(text => { + text.style.color = isDark ? '#C8CBE7' : '#494C6B'; + }); + + // 로컬 스토리지에 테마 상태 저장 + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + + // Oval 상태 업데이트 + ovalState = isDark ? 'dark' : 'light'; + localStorage.setItem('ovalState', ovalState); + updateAddTodoOval(); + updateTodoList(); +}; + + +// 초기 로딩 시 테마 및 할 일 설정 +document.addEventListener('DOMContentLoaded', function () { + applyDarkModeShadow(); + const savedTheme = localStorage.getItem('theme'); + const isDark = savedTheme === 'dark'; + + const filterButtons = document.querySelectorAll('[data-filter]'); + filterButtons.forEach(button => { + button.addEventListener('click', function() { + const filter = this.dataset.filter; + + // 필터를 클릭할 때마다 로컬 스토리지에 저장 + localStorage.setItem('filter', filter); + + // 필터링된 할 일 목록을 화면에 갱신 + filterTodos(filter); + + // 선택된 필터 버튼에 active, hover 클래스를 추가하여 스타일 적용 + filterButtons.forEach(btn => btn.classList.remove('active', 'hover')); + this.classList.add('active', 'hover'); + }); + }); + const clearCompletedButton = document.querySelector('#clear-completed'); // clear completed 버튼 선택 + if (clearCompletedButton) { + clearCompletedButton.addEventListener('click', function() { + // 완료된 할 일을 삭제하는 함수 호출 + clearCompletedTodos(); + }); + } + + if (isDark) { + document.body.classList.add('dark'); + themeIcon.src = 'images/icon-sun.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-dark.jpg)'; + document.body.style.background = '#171823'; + const containerBgColor = '#25273D'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + } else { + document.body.classList.remove('dark'); + themeIcon.src = 'images/icon-moon.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-light.jpg)'; + document.body.style.background = '#FAFAFA'; + const containerBgColor = '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + } + + updateAddTodoOval(); + + // 로컬 스토리지에서 필터 상태 가져오기 (기본값: 'all') + const savedFilter = localStorage.getItem('all') || 'all'; + const activeButton = document.querySelector(`[data-filter='${savedFilter}']`); + if (activeButton) { + // 선택된 필터 버튼에 active, hover 클래스를 추가하여 스타일 적용 + activeButton.classList.add('active', 'hover'); + } + + // 해당 필터에 맞게 할 일 목록 필터링 + filterTodos(savedFilter); // 필터링 함수 호출 + + // 로컬 스토리지에서 할 일 목록 가져오기 + const savedTodos = JSON.parse(localStorage.getItem('todos')); + if (savedTodos) { + todos = savedTodos; // 로컬 스토리지에서 가져온 todos 배열로 갱신 + + // 필터 상태에 맞게 할 일 목록을 필터링하고 갱신 + filterTodos(savedFilter); // 필터링된 상태로 할 일 목록 갱신 + updateTodoList(); // 필터링된 할 일 목록을 화면에 표시 + updateAddTodoOval(); // Oval 업데이트 함수 호출 + } + applyDarkModeShadow(); // 테마에 맞는 그림자 적용 +}); + +// 테마 토글 버튼 이벤트 +themeToggle.addEventListener('click', toggleTheme); + +// 새로운 할 일 추가 +newTodoInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && newTodoInput.value.trim()) { + addTodo(newTodoInput.value); + newTodoInput.value = ''; + } +}); + +// 할 일 추가 함수 +function addTodo(text) { + const todo = { text, completed: false }; + todos.push(todo); + updateTodoList(); + saveTodosToLocalStorage(); +} + +function handleDragStart(event) { + draggedIndex = event.target.getAttribute('data-index'); + event.dataTransfer.effectAllowed = "move"; +} + +function handleDragOver(event) { + event.preventDefault(); // 드롭을 허용하기 위해 필요 + event.dataTransfer.dropEffect = "move"; +} + +function handleDrop(event) { + event.preventDefault(); + const targetIndex = event.target.closest('.todo-item').getAttribute('data-index'); + + // todos 배열을 드래그한 항목과 드롭한 대상의 위치에 맞게 재정렬 + if (draggedIndex !== null && targetIndex !== null && draggedIndex !== targetIndex) { + const movedItem = todos.splice(draggedIndex, 1)[0]; + todos.splice(targetIndex, 0, movedItem); + + updateTodoList(); + saveTodosToLocalStorage(); + } + draggedIndex = null; +} + + + +// updateTodoList 함수에서 새로 생성되는 항목의 색상 설정 +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; + todoItem.setAttribute('data-index', index); + + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); +} + + +// 할 일 목록 업데이트 함수 (드래그 앤 드롭 통합) +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; // 드래그 가능하도록 설정 + todoItem.setAttribute('data-index', index); + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + const cancelButton = todoItem.querySelector('.cross-line'); + + // 완료된 할 일 텍스트 스타일 적용 + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + // 드래그 앤 드롭 이벤트 추가 + todoItem.addEventListener('dragstart', handleDragStart); + todoItem.addEventListener('dragover', handleDragOver); + todoItem.addEventListener('drop', handleDrop); + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + cancelButton.addEventListener('click', (e) => { + e.stopPropagation(); + deleteTodo(index); + }); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); +} + + +// 할 일 삭제 함수 +function deleteTodo(index) { + todos.splice(index, 1); + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 완료 상태 토글 함수 +function toggleComplete(index) { + todos[index].completed = !todos[index].completed; + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 남은 할 일 수 업데이트 +function updateItemsLeft() { + const remainingItems = todos.filter(todo => !todo.completed).length; + itemsLeft.textContent = `${remainingItems} items left`; +} + +// 필터링 함수에서 완료된 항목 스타일 수정 + +// 필터링 함수에서 완료된 항목 스타일 수정 +function filterTodos(filter) { + mainContainer.innerHTML = ''; // 기존 항목을 제거 + const isDark = document.body.classList.contains('dark'); + + // 필터링된 할 일 항목을 가져옵니다. + const filteredTodos = filter === 'all' ? todos : + todos.filter(todo => filter === 'active' ? !todo.completed : todo.completed); + + // 필터링된 항목을 표시합니다. + filteredTodos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; + todoItem.setAttribute('data-index', index); // data-index로 인덱스를 추가 + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + + `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + // 완료된 항목은 스타일을 변경 + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + // mainContainer에 todo 항목 추가 + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); // 남은 할 일 수 업데이트 +} + + +function renderTodos() { + // 기존 필터된 todos 배열을 기반으로 필터링된 todo만 렌더링 + const filteredTodos = todos.filter(todo => { + if (currentFilter === 'active') { + return !todo.completed; + } else if (currentFilter === 'completed') { + return todo.completed; + } else { + return true; // 'all' 필터는 모든 항목을 보여줌 + } + }); + + // 필터링된 todos를 화면에 렌더링 + mainContainer.innerHTML = ''; // 기존 내용 초기화 +} + + +// 완료된 할 일 삭제 +clearCompletedButton.addEventListener('click', () => { + todos = todos.filter(todo => !todo.completed); + updateTodoList(); + renderTodos(); + saveTodosToLocalStorage(); + +}); + +// 로컬 스토리지 저장 +function saveTodosToLocalStorage() { + localStorage.setItem('todos', JSON.stringify(todos)); +} + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..618bd77 --- /dev/null +++ b/style.css @@ -0,0 +1,271 @@ +.attribution { font-size: 11px; text-align: center; } + +body { + width: 100%; + background: #fafafa; + font-family: "Josefin Sans", sans-serif; + display: flex; + align-items: center; + flex-direction: column; +} + +#background-image { + position:absolute; + background-image: url("images/bg-desktop-light.jpg"); + background-position: center; + top: 0; + left: 0px; + width: 100%; + height: 300px; + z-index: -1; + background-repeat: no-repeat; + background-size: cover; +} + +.header-container{ + width: 541px; + height: 48px; + flex-shrink: 0; + margin-top: 70px; + display: flex; + justify-content: space-between; +} + +.header{ + color: #FFF; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: 15px; + margin: 0; +} + + +.input-container{ + max-width: 540px; + width: 90%; + height: 64px; + margin-top: 40px; + flex-shrink: 0; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + align-items: center; + flex-direction: row; +} + +.oval{ + width: 24px; + height: 24px; + fill: #FFF; +} + +.todo-item .oval:hover circle{ + stroke:#5df; +} + + + + +#input-text{ + color : #393A4B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; + border: none; + outline: none; +} + +.input-box{ + display: inline-flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-left: 24px; +} + +.main-container{ + max-width: 540px; + width: 90%; + min-height: 0px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + margin-top: 24px; +} + +.bottom-container{ + max-width: 540px; + width: 90%; + max-height: 439px; + min-height: 50px; + flex-shrink: 0; + display: flex; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + margin-bottom: 16px; + padding: 0 24px; + box-sizing: border-box; +} +body.dark.bottom-container{ + max-width: 540px; + width: 90%; + max-height: 439px; + min-height: 50px; + flex-shrink: 0; + display: flex; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); + justify-content: space-between; + align-items: center; + margin-top: auto; + margin-bottom: 16px; + padding: 0 24px; + box-sizing: border-box; +} +body.dark.main-container{ + max-width: 540px; + width: 90%; + min-height: 0px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); + margin-top: 24px; +} + +.toggle { + background: none; /* 배경을 없애거나 단색으로 설정 */ + border: none; /* 테두리 없애기 */ + box-shadow: none; /* 그림자 없애기 */ + padding: 0; /* 기본 여백 없애기 */ + outline: none; /* 클릭 시 외곽선 없애기 */ + cursor: pointer; /* 포인터 변경 */ +} + +.interaction-container{ + width: 116px; + height: 14px; + display: inline-flex; + padding-bottom: 4px; + justify-content: center; + align-items: flex-start; + gap: 19px; + color: #9495A5; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; + margin-left: 40px; +} + +.bottom{ + color: #9495A5; + text-align: right; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + + + +.footer{ + margin-top: 49px; + color: #9495A5; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + +.todo-item{ + display: flex; + width: 492px; + height: 24px; + flex-shrink: 0; + align-items: center; + margin-top: 20px; +} + +.todo-text{ + margin-left: 24px; + color: #494C6B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; +} + +.line{ + margin-top: 20px; + width: 540px; + height: 1px; + flex-shrink: 0; + background: #E3E4F1; +} + +.cross-line{ + margin-left: auto; + display: none; +} + +/* todo-item에 마우스를 올렸을 때만 cross-line 보이기 */ +.todo-item:hover .cross-line { + display: block; +} + + + +.filter-button{ + border: none; + outline: none; + background: none; + color: #9495A5; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; +} + +.clear{ + color: #9495A5; + text-align: right; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + + +.filter-button.active{ + color: #3A7CFD; +} \ No newline at end of file diff --git a/todo_images/bg-desktop-dark.jpg b/todo_images/bg-desktop-dark.jpg new file mode 100644 index 0000000..394ebb9 Binary files /dev/null and b/todo_images/bg-desktop-dark.jpg differ diff --git a/todo_images/bg-desktop-light.jpg b/todo_images/bg-desktop-light.jpg new file mode 100644 index 0000000..1b5f3bb Binary files /dev/null and b/todo_images/bg-desktop-light.jpg differ diff --git a/todo_images/bg-mobile-dark.jpg b/todo_images/bg-mobile-dark.jpg new file mode 100644 index 0000000..3285a35 Binary files /dev/null and b/todo_images/bg-mobile-dark.jpg differ diff --git a/todo_images/bg-mobile-light.jpg b/todo_images/bg-mobile-light.jpg new file mode 100644 index 0000000..9df5c53 Binary files /dev/null and b/todo_images/bg-mobile-light.jpg differ diff --git a/todo_images/favicon-32x32.png b/todo_images/favicon-32x32.png new file mode 100644 index 0000000..1e2df7f Binary files /dev/null and b/todo_images/favicon-32x32.png differ diff --git a/todo_images/icon-check.svg b/todo_images/icon-check.svg new file mode 100644 index 0000000..61e7384 --- /dev/null +++ b/todo_images/icon-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/todo_images/icon-cross.svg b/todo_images/icon-cross.svg new file mode 100644 index 0000000..cdf9c7c --- /dev/null +++ b/todo_images/icon-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/todo_images/icon-moon.svg b/todo_images/icon-moon.svg new file mode 100644 index 0000000..60c2ace --- /dev/null +++ b/todo_images/icon-moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/todo_images/icon-sun.svg b/todo_images/icon-sun.svg new file mode 100644 index 0000000..24f69f3 --- /dev/null +++ b/todo_images/icon-sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/todo_index.html.html b/todo_index.html.html new file mode 100644 index 0000000..2769508 --- /dev/null +++ b/todo_index.html.html @@ -0,0 +1,61 @@ + + + + + + + + + + Frontend Mentor | Todo app + + + + + + + + + +
+ +
+
TODO
+ +
+ +
+
+
+ +
+
+ +
+
+ ${todo.completed ? clickedOval : (isDark ? darkOvalSVG : lightOvalSVG)} +
+ + ${todo.text} + +
+
+
0 items left
+
+ + + +
+ +
+ + + + + \ No newline at end of file diff --git a/todo_script.js b/todo_script.js new file mode 100644 index 0000000..a57aaea --- /dev/null +++ b/todo_script.js @@ -0,0 +1,458 @@ + +// 주요 DOM 요소 가져오기 +const themeToggle = document.querySelector('.toggle'); // 테마 전환 버튼 +const themeIcon = document.getElementById('icon'); // 테마 아이콘 +const background = document.getElementById('background-image'); // 배경 이미지 +const newTodoInput = document.getElementById('input-text'); // 새로운 할 일 입력 필드 +const mainContainer = document.querySelector('.main-container'); // 할 일 목록이 표시될 메인 컨테이너 +const itemsLeft = document.querySelector('.remain-item'); // 남은 할 일 수 표시 +const clearCompletedButton = document.querySelector('.clear'); // 완료된 할 일 삭제 버튼 +const filterButtons = document.querySelectorAll('.filter-button'); // 필터 버튼들 +const addTodoOval = document.querySelector('.oval'); // 할 일 추가 버튼의 Oval +const inputContainer = document.querySelector('.input-container'); // 입력 컨테이너 +const bottomContainer = document.querySelector('.bottom-container'); // 하단 컨테이너 + + +let todos = []; + +let draggedIndex = null; + +// Oval 상태 로컬 스토리지에서 가져오기 +let ovalState = localStorage.getItem('ovalState') || 'light'; + +// 기본, hover, 완료 상태 SVG +const lightOvalSVG = ` + + + `; + +const darkOvalSVG = ` + + + `; + +const clickedOval = ` + + + + + + + + + + + `; + +const hoverOval = ` + + + + + + + + + + + + + + + + + `; + + //다크모드 그림자 +function applyDarkModeShadow() { + if (document.body.classList.contains('dark')) { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(0, 0, 0, 0.50)'; + } else { + mainContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + bottomContainer.style.boxShadow = '0px 35px 50px -15px rgba(194, 195, 214, 0.50)'; + } +} + +// Oval 업데이트 함수 +function updateAddTodoOval() { + const isDark = document.body.classList.contains('dark'); + addTodoOval.innerHTML = isDark ? darkOvalSVG : lightOvalSVG; +} + +// 테마 전환 함수에서 line과 텍스트 색상 업데이트 추가 +const toggleTheme = () => { + document.body.classList.toggle('dark'); + const isDark = document.body.classList.contains('dark'); + + // 아이콘과 배경 이미지 변경 + themeIcon.src = isDark ? 'images/icon-sun.svg' : 'images/icon-moon.svg'; + background.style.backgroundImage = isDark + ? 'url(images/bg-desktop-dark.jpg)' + : 'url(images/bg-desktop-light.jpg)'; + + // 낮/밤 모드에 따른 배경색 변경 + document.body.style.background = isDark ? '#171823' : '#FAFAFA'; + + // 주요 컨테이너 배경색 + const containerBgColor = isDark ? '#25273D' : '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + + // line 및 텍스트 색상 업데이트 + document.querySelectorAll('.line').forEach(line => { + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + }); + document.querySelectorAll('.todo-text').forEach(text => { + text.style.color = isDark ? '#C8CBE7' : '#494C6B'; + }); + + // 로컬 스토리지에 테마 상태 저장 + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + + // Oval 상태 업데이트 + ovalState = isDark ? 'dark' : 'light'; + localStorage.setItem('ovalState', ovalState); + updateAddTodoOval(); + updateTodoList(); +}; + + +// 초기 로딩 시 테마 및 할 일 설정 +document.addEventListener('DOMContentLoaded', function () { + applyDarkModeShadow(); + const savedTheme = localStorage.getItem('theme'); + const isDark = savedTheme === 'dark'; + + const filterButtons = document.querySelectorAll('[data-filter]'); + filterButtons.forEach(button => { + button.addEventListener('click', function() { + const filter = this.dataset.filter; + + // 필터를 클릭할 때마다 로컬 스토리지에 저장 + localStorage.setItem('filter', filter); + + // 필터링된 할 일 목록을 화면에 갱신 + filterTodos(filter); + + // 선택된 필터 버튼에 active, hover 클래스를 추가하여 스타일 적용 + filterButtons.forEach(btn => btn.classList.remove('active', 'hover')); + this.classList.add('active', 'hover'); + }); + }); + const clearCompletedButton = document.querySelector('#clear-completed'); // clear completed 버튼 선택 + if (clearCompletedButton) { + clearCompletedButton.addEventListener('click', function() { + // 완료된 할 일을 삭제하는 함수 호출 + clearCompletedTodos(); + }); + } + + if (isDark) { + document.body.classList.add('dark'); + themeIcon.src = 'images/icon-sun.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-dark.jpg)'; + document.body.style.background = '#171823'; + const containerBgColor = '#25273D'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + } else { + document.body.classList.remove('dark'); + themeIcon.src = 'images/icon-moon.svg'; + background.style.backgroundImage = 'url(images/bg-desktop-light.jpg)'; + document.body.style.background = '#FAFAFA'; + const containerBgColor = '#FFFFFF'; + inputContainer.style.background = containerBgColor; + newTodoInput.style.background = containerBgColor; + mainContainer.style.background = containerBgColor; + bottomContainer.style.background = containerBgColor; + } + + updateAddTodoOval(); + + // 로컬 스토리지에서 필터 상태 가져오기 (기본값: 'all') + const savedFilter = localStorage.getItem('all') || 'all'; + const activeButton = document.querySelector(`[data-filter='${savedFilter}']`); + if (activeButton) { + // 선택된 필터 버튼에 active, hover 클래스를 추가하여 스타일 적용 + activeButton.classList.add('active', 'hover'); + } + + // 해당 필터에 맞게 할 일 목록 필터링 + filterTodos(savedFilter); // 필터링 함수 호출 + + // 로컬 스토리지에서 할 일 목록 가져오기 + const savedTodos = JSON.parse(localStorage.getItem('todos')); + if (savedTodos) { + todos = savedTodos; // 로컬 스토리지에서 가져온 todos 배열로 갱신 + + // 필터 상태에 맞게 할 일 목록을 필터링하고 갱신 + filterTodos(savedFilter); // 필터링된 상태로 할 일 목록 갱신 + updateTodoList(); // 필터링된 할 일 목록을 화면에 표시 + updateAddTodoOval(); // Oval 업데이트 함수 호출 + } + applyDarkModeShadow(); // 테마에 맞는 그림자 적용 +}); + +// 테마 토글 버튼 이벤트 +themeToggle.addEventListener('click', toggleTheme); + +// 새로운 할 일 추가 +newTodoInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter' && newTodoInput.value.trim()) { + addTodo(newTodoInput.value); + newTodoInput.value = ''; + } +}); + +// 할 일 추가 함수 +function addTodo(text) { + const todo = { text, completed: false }; + todos.push(todo); + updateTodoList(); + saveTodosToLocalStorage(); +} + +function handleDragStart(event) { + draggedIndex = event.target.getAttribute('data-index'); + event.dataTransfer.effectAllowed = "move"; +} + +function handleDragOver(event) { + event.preventDefault(); // 드롭을 허용하기 위해 필요 + event.dataTransfer.dropEffect = "move"; +} + +function handleDrop(event) { + event.preventDefault(); + const targetIndex = event.target.closest('.todo-item').getAttribute('data-index'); + + // todos 배열을 드래그한 항목과 드롭한 대상의 위치에 맞게 재정렬 + if (draggedIndex !== null && targetIndex !== null && draggedIndex !== targetIndex) { + const movedItem = todos.splice(draggedIndex, 1)[0]; + todos.splice(targetIndex, 0, movedItem); + + updateTodoList(); + saveTodosToLocalStorage(); + } + draggedIndex = null; +} + + + +// updateTodoList 함수에서 새로 생성되는 항목의 색상 설정 +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; + todoItem.setAttribute('data-index', index); + + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); +} + + +// 할 일 목록 업데이트 함수 (드래그 앤 드롭 통합) +function updateTodoList() { + mainContainer.innerHTML = ''; // 기존 목록 초기화 + const isDark = document.body.classList.contains('dark'); + + todos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; // 드래그 가능하도록 설정 + todoItem.setAttribute('data-index', index); + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + +
+ + + +
+ `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + const cancelButton = todoItem.querySelector('.cross-line'); + + // 완료된 할 일 텍스트 스타일 적용 + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + // 드래그 앤 드롭 이벤트 추가 + todoItem.addEventListener('dragstart', handleDragStart); + todoItem.addEventListener('dragover', handleDragOver); + todoItem.addEventListener('drop', handleDrop); + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + cancelButton.addEventListener('click', (e) => { + e.stopPropagation(); + deleteTodo(index); + }); + + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); +} + + +// 할 일 삭제 함수 +function deleteTodo(index) { + todos.splice(index, 1); + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 완료 상태 토글 함수 +function toggleComplete(index) { + todos[index].completed = !todos[index].completed; + updateTodoList(); + saveTodosToLocalStorage(); +} + +// 남은 할 일 수 업데이트 +function updateItemsLeft() { + const remainingItems = todos.filter(todo => !todo.completed).length; + itemsLeft.textContent = `${remainingItems} items left`; +} + +// 필터링 함수에서 완료된 항목 스타일 수정 + +// 필터링 함수에서 완료된 항목 스타일 수정 +function filterTodos(filter) { + mainContainer.innerHTML = ''; // 기존 항목을 제거 + const isDark = document.body.classList.contains('dark'); + + // 필터링된 할 일 항목을 가져옵니다. + const filteredTodos = filter === 'all' ? todos : + todos.filter(todo => filter === 'active' ? !todo.completed : todo.completed); + + // 필터링된 항목을 표시합니다. + filteredTodos.forEach((todo, index) => { + const todoItem = document.createElement('div'); + todoItem.className = 'todo-item'; + todoItem.draggable = true; + todoItem.setAttribute('data-index', index); // data-index로 인덱스를 추가 + + todoItem.innerHTML = ` +
+ ${todo.completed ? clickedOval : isDark ? darkOvalSVG : lightOvalSVG} +
+ + ${todo.text} + + `; + + const oval = todoItem.querySelector('.oval'); + const todoText = todoItem.querySelector('.todo-text'); + + // 완료된 항목은 스타일을 변경 + if (todo.completed) { + todoText.style.textDecoration = 'line-through'; + todoText.style.color = isDark ? '#4D5067' : '#9495A5'; + oval.innerHTML = clickedOval; + } + + oval.addEventListener('click', () => toggleComplete(index)); + todoText.addEventListener('click', () => toggleComplete(index)); + + // mainContainer에 todo 항목 추가 + mainContainer.appendChild(todoItem); + + const line = document.createElement('div'); + line.className = 'line'; + line.style.background = isDark ? '#393A4B' : '#E3E4F1'; + mainContainer.appendChild(line); + }); + + updateItemsLeft(); // 남은 할 일 수 업데이트 +} + + +function renderTodos() { + // 기존 필터된 todos 배열을 기반으로 필터링된 todo만 렌더링 + const filteredTodos = todos.filter(todo => { + if (currentFilter === 'active') { + return !todo.completed; + } else if (currentFilter === 'completed') { + return todo.completed; + } else { + return true; // 'all' 필터는 모든 항목을 보여줌 + } + }); + + // 필터링된 todos를 화면에 렌더링 + mainContainer.innerHTML = ''; // 기존 내용 초기화 +} + + +// 완료된 할 일 삭제 +clearCompletedButton.addEventListener('click', () => { + todos = todos.filter(todo => !todo.completed); + updateTodoList(); + renderTodos(); + saveTodosToLocalStorage(); + +}); + +// 로컬 스토리지 저장 +function saveTodosToLocalStorage() { + localStorage.setItem('todos', JSON.stringify(todos)); +} + diff --git a/todo_sytle.css b/todo_sytle.css new file mode 100644 index 0000000..3b57bf9 --- /dev/null +++ b/todo_sytle.css @@ -0,0 +1,272 @@ + +.attribution { font-size: 11px; text-align: center; } + +body { + width: 100%; + background: #fafafa; + font-family: "Josefin Sans", sans-serif; + display: flex; + align-items: center; + flex-direction: column; +} + +#background-image { + position:absolute; + background-image: url("images/bg-desktop-light.jpg"); + background-position: center; + top: 0; + left: 0px; + width: 100%; + height: 300px; + z-index: -1; + background-repeat: no-repeat; + background-size: cover; +} + +.header-container{ + width: 541px; + height: 48px; + flex-shrink: 0; + margin-top: 70px; + display: flex; + justify-content: space-between; +} + +.header{ + color: #FFF; + font-size: 40px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: 15px; + margin: 0; +} + + +.input-container{ + max-width: 540px; + width: 90%; + height: 64px; + margin-top: 40px; + flex-shrink: 0; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + align-items: center; + flex-direction: row; +} + +.oval{ + width: 24px; + height: 24px; + fill: #FFF; +} + +.todo-item .oval:hover circle{ + stroke:#5df; +} + + + + +#input-text{ + color : #393A4B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; + border: none; + outline: none; +} + +.input-box{ + display: inline-flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-left: 24px; +} + +.main-container{ + max-width: 540px; + width: 90%; + min-height: 0px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + margin-top: 24px; +} + +.bottom-container{ + max-width: 540px; + width: 90%; + max-height: 439px; + min-height: 50px; + flex-shrink: 0; + display: flex; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(194, 195, 214, 0.50); + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + margin-bottom: 16px; + padding: 0 24px; + box-sizing: border-box; +} +body.dark.bottom-container{ + max-width: 540px; + width: 90%; + max-height: 439px; + min-height: 50px; + flex-shrink: 0; + display: flex; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); + justify-content: space-between; + align-items: center; + margin-top: auto; + margin-bottom: 16px; + padding: 0 24px; + box-sizing: border-box; +} +body.dark.main-container{ + max-width: 540px; + width: 90%; + min-height: 0px; + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 5px; + background: #FFF; + box-shadow: 0px 35px 50px -15px rgba(0, 0, 0, 0.50); + margin-top: 24px; +} + +.toggle { + background: none; /* 배경을 없애거나 단색으로 설정 */ + border: none; /* 테두리 없애기 */ + box-shadow: none; /* 그림자 없애기 */ + padding: 0; /* 기본 여백 없애기 */ + outline: none; /* 클릭 시 외곽선 없애기 */ + cursor: pointer; /* 포인터 변경 */ +} + +.interaction-container{ + width: 116px; + height: 14px; + display: inline-flex; + padding-bottom: 4px; + justify-content: center; + align-items: flex-start; + gap: 19px; + color: #9495A5; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; + margin-left: 40px; +} + +.bottom{ + color: #9495A5; + text-align: right; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + + + +.footer{ + margin-top: 49px; + color: #9495A5; + text-align: center; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + +.todo-item{ + display: flex; + width: 492px; + height: 24px; + flex-shrink: 0; + align-items: center; + margin-top: 20px; +} + +.todo-text{ + margin-left: 24px; + color: #494C6B; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.25px; +} + +.line{ + margin-top: 20px; + width: 540px; + height: 1px; + flex-shrink: 0; + background: #E3E4F1; +} + +.cross-line{ + margin-left: auto; + display: none; +} + +/* todo-item에 마우스를 올렸을 때만 cross-line 보이기 */ +.todo-item:hover .cross-line { + display: block; +} + + + +.filter-button{ + border: none; + outline: none; + background: none; + color: #9495A5; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + letter-spacing: -0.194px; +} + +.clear{ + color: #9495A5; + text-align: right; + font-family: "Josefin Sans"; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: -0.194px; +} + + +.filter-button.active{ + color: #3A7CFD; +} \ No newline at end of file