From bcfcb93e9c0a51f9895dda2fa4fa61a76472228e Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 14:22:56 +0900 Subject: [PATCH 01/57] docs: make feature list --- todo.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 todo.md diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..c35f4fa --- /dev/null +++ b/todo.md @@ -0,0 +1,24 @@ +# 피쳐 리스트 + +| priority | 기능 | 태스크 | +| -------- | ---------------------------------- | ----------------------------------- | +| high | **기본 레이아웃** | 헤더, column(open, done) 배치 | +| | | CSS 스타일 적용 | +| high | **데이터 구조 작성** | 카드 및 태스크 class 정의 | +| high | **카드 추가** | 카드 추가 버튼과 모달 구현 | +| | | 카드 추가 버튼 이벤트 핸들러 작성 | +| high | **카드 삭제** | 카드 삭제 버튼 구현 | +| | | 카드 삭제 버튼 이벤트 핸들러 작성 | +| high | **카드 상태 토글** | 카드 완료 체크박스 추가 | +| | | 카드 상태에 따른 스타일 변경 | +| | | 카드 상태 변경 이벤트 핸들러 작성 | +| high | **태스크 추가** | 태스크 input 필드 구현 | +| | | submit 이벤트 핸들러 작성 | +| high | **태스크 삭제** | 태스크 삭제 버튼 구현 | +| | | submit 삭제 버튼 이벤트 핸들러 작성 | +| high | **태스크 상태 토글** | 태스크 완료 체크박스 추가 | +| | | 태스크 상태에 따른 스타일 변경 | +| | | 태스크 상태 변경 이벤트 핸들러 작성 | +| medium | **드래그 앤 드롭** | 드래그 앤 드롭 이벤트 핸들러 작성 | +| low | **로컬 스토리지 저장 및 불러오기** | 보드 상태 저장 함수 작성 | +| | | 페이지 로드 시 저장된 보드 불러오기 | From 9b52a9933479a56129b0e32dc66ae7c36697f185 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 14:30:15 +0900 Subject: [PATCH 02/57] chore: add prettier config --- .prettierrc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7488843 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "htmlWhitespaceSensitivity": "css", + "cssWhitespaceSensitivity": "css" +} From 2ffbddf3621e3aa34600dd5189d8023ef6fe1ae8 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 14:30:48 +0900 Subject: [PATCH 03/57] chore: add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file From 810a8500627961af82a28389d481dd678f1d5517 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 14:50:30 +0900 Subject: [PATCH 04/57] feat: make HTML structure --- index.html | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index d241b1b..15c3a8b 100644 --- a/index.html +++ b/index.html @@ -4,11 +4,52 @@ Vanilla Todo - + -
+
+ +
+
+

Open

+
    +
  • +
    Card Title 1
    +
    +
      +
    1. task 1
    2. +
    3. task 2
    4. +
    +
    +
  • +
  • +
    Card Title 2
    +
    +
      +
    1. task 1
    2. +
    3. task 2
    4. +
    +
    +
  • +
+
+
+

Done

+
    +
  • +
    Card Title 1
    +
    +
      +
    1. task 1
    2. +
    3. task 2
    4. +
    +
    +
  • +
+
+
+
- + From 2a32b4f2b62b74793a921dff5c6d4012bb8d0e15 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 14:51:22 +0900 Subject: [PATCH 05/57] chore: reorganize project directory structure & add reset.css --- css/reset.css | 14 ++++++++++++++ js/index.js | 1 + script.js | 1 - style.css | 1 - 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 css/reset.css create mode 100644 js/index.js delete mode 100644 script.js delete mode 100644 style.css diff --git a/css/reset.css b/css/reset.css new file mode 100644 index 0000000..d2ccfd0 --- /dev/null +++ b/css/reset.css @@ -0,0 +1,14 @@ +/* 기본 margin, padding 제거 */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* 리스트 스타일 제거 */ +ul, +ol { + list-style: none; +} diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..4ff0861 --- /dev/null +++ b/js/index.js @@ -0,0 +1 @@ +console.log('작성 시작'); diff --git a/script.js b/script.js deleted file mode 100644 index 355dcc2..0000000 --- a/script.js +++ /dev/null @@ -1 +0,0 @@ -//😍CEOS 20기 프론트엔드 파이팅😍 diff --git a/style.css b/style.css deleted file mode 100644 index 599136a..0000000 --- a/style.css +++ /dev/null @@ -1 +0,0 @@ -/* 본인의 디자인 감각을 최대한 발휘해주세요! */ From 4efb3f6e6f7f21451d9eb7d1b7e98653e8a93f6d Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 15:52:42 +0900 Subject: [PATCH 06/57] style: write css files to style entire page --- css/card.css | 14 +++++++++++ css/layout.css | 61 +++++++++++++++++++++++++++++++++++++++++++++++ css/task.css | 5 ++++ css/variables.css | 13 ++++++++++ index.html | 58 ++++++++++++++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 css/card.css create mode 100644 css/layout.css create mode 100644 css/task.css create mode 100644 css/variables.css diff --git a/css/card.css b/css/card.css new file mode 100644 index 0000000..d1d3d2a --- /dev/null +++ b/css/card.css @@ -0,0 +1,14 @@ +.card-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.card { + padding: 1rem; + + background: var(--card-background); + + border: 2px solid var(--card-border); + border-radius: 1rem; +} diff --git a/css/layout.css b/css/layout.css new file mode 100644 index 0000000..5ea7a0b --- /dev/null +++ b/css/layout.css @@ -0,0 +1,61 @@ +body { + background-color: var(--background); + color: var(--text-color); +} + +.container { + display: 'flex'; + padding: 2rem; +} + +.header { + margin-bottom: 1rem; +} +header > h1 { + font-size: 3rem; +} + +.title { + text-align: center; + + color: transparent; + -webkit-text-stroke: 1px var(--text-color); +} + +.kanban-board { + display: flex; + gap: 0.5rem; + + width: 100%; +} + +.board-column { + display: flex; + flex-direction: column; + gap: 1rem; + + flex: 1; + + padding: 1rem; + + border: 3px solid; + border-radius: 1rem; +} + +#open-column { + background-color: var(--open-column-background); + border-color: var(--open-column-border); +} + +#done-column { + background-color: var(--done-column-background); + border-color: var(--done-column-border); +} + +.open-hr { + border: 1px solid var(--open-column-border); +} + +.done-hr { + border: 1px solid var(--done-column-border); +} diff --git a/css/task.css b/css/task.css new file mode 100644 index 0000000..89a723f --- /dev/null +++ b/css/task.css @@ -0,0 +1,5 @@ +.task { + display: flex; + align-items: center; + gap: 0.5rem; +} diff --git a/css/variables.css b/css/variables.css new file mode 100644 index 0000000..9c4bf15 --- /dev/null +++ b/css/variables.css @@ -0,0 +1,13 @@ +:root { + --background: #121212; + + --open-column-background: #31627e1a; + --done-column-background: #2a5f2d1a; + --open-column-border: #0d5580; + --done-column-border: #0b5c18; + + --card-background: #121212ee; + --card-border: #505050; + + --text-color: #ffffff; +} diff --git a/index.html b/index.html index 15c3a8b..cdba29c 100644 --- a/index.html +++ b/index.html @@ -1,48 +1,80 @@ - + Vanilla Todo + + + +
- +
+

20XX. XX. XX.

+
+
-
+

Open

+ +
+
  • -
    Card Title 1
    +

    Card Title 1

      -
    1. task 1
    2. -
    3. task 2
    4. +
    5. + +

      task 1

      +
    6. +
    7. + +

      task 2

      +
  • -
    Card Title 2
    +

    Card Title 2

      -
    1. task 1
    2. -
    3. task 2
    4. +
    5. + +

      task 1

      +
    6. +
    7. + +

      task 2

      +
-
+ +

Done

+ +
+
  • -
    Card Title 1
    +

    Card Title 1

      -
    1. task 1
    2. -
    3. task 2
    4. +
    5. + +

      task 1

      +
    6. +
    7. + +

      task 2

      +
  • From 868e2af9f367c18a1ef7c9818f7ff19c6a9ff2c9 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 16:15:56 +0900 Subject: [PATCH 07/57] feat: define basic subject, task model --- js/index.js | 13 ++++++++++++- js/models/subject.js | 10 ++++++++++ js/models/task.js | 7 +++++++ js/viewModels/subjectViewModel.js | 14 ++++++++++++++ js/viewModels/taskViewModel.js | 0 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 js/models/subject.js create mode 100644 js/models/task.js create mode 100644 js/viewModels/subjectViewModel.js create mode 100644 js/viewModels/taskViewModel.js diff --git a/js/index.js b/js/index.js index 4ff0861..5029ab4 100644 --- a/js/index.js +++ b/js/index.js @@ -1 +1,12 @@ -console.log('작성 시작'); +document.addEventListener('DOMContentLoaded', () => { + const subjectViewModel = new SubjectViewModel(); + + const openSubject1 = new Subject('Opened Subject 1'); + openSubject1.addTask(new Task('task 1')); + openSubject1.addTask(new Task('task 2')); + subjectViewModel.addCard(openSubject1); + + const openSubject2 = new Subject('Opened Subject 2'); + openSubject2.addTask(new Task('task 1')); + subjectViewModel.addCard(openSubject2); +}); diff --git a/js/models/subject.js b/js/models/subject.js new file mode 100644 index 0000000..8ccf6f8 --- /dev/null +++ b/js/models/subject.js @@ -0,0 +1,10 @@ +class Subject { + constructor(title = 'New Subject') { + this.title = title; + this.taskList = []; + } + + addTask(task) { + this.taskList.push(task); + } +} diff --git a/js/models/task.js b/js/models/task.js new file mode 100644 index 0000000..d2e57b7 --- /dev/null +++ b/js/models/task.js @@ -0,0 +1,7 @@ +class Task { + constructor(title) { + this.title = title; + } + + render() {} +} diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js new file mode 100644 index 0000000..a0bd6a9 --- /dev/null +++ b/js/viewModels/subjectViewModel.js @@ -0,0 +1,14 @@ +class SubjectViewModel { + constructor() { + this.subjectList = []; + } + + addSubject(subject) { + this.subjectList.push(subject); + this.render(); + } + + render() { + // render 로직 + } +} diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js new file mode 100644 index 0000000..e69de29 From 56b80ad7c6f5d5e4f0fc3901e0a0e2ef77acd79b Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 16:52:41 +0900 Subject: [PATCH 08/57] feat: render subject card --- index.html | 55 ++++--------------------------- js/constant.js | 2 ++ js/index.js | 13 +++++--- js/models/subject.js | 11 ++++++- js/models/task.js | 2 -- js/viewModels/subjectViewModel.js | 26 ++++++++++++++- 6 files changed, 52 insertions(+), 57 deletions(-) create mode 100644 js/constant.js diff --git a/index.html b/index.html index cdba29c..41fab25 100644 --- a/index.html +++ b/index.html @@ -23,38 +23,7 @@

    Open


    -
      -
    • -

      Card Title 1

      -
      -
        -
      1. - -

        task 1

        -
      2. -
      3. - -

        task 2

        -
      4. -
      -
      -
    • -
    • -

      Card Title 2

      -
      -
        -
      1. - -

        task 1

        -
      2. -
      3. - -

        task 2

        -
      4. -
      -
      -
    • -
    +
    @@ -62,26 +31,14 @@

    Done


    -
      -
    • -

      Card Title 1

      -
      -
        -
      1. - -

        task 1

        -
      2. -
      3. - -

        task 2

        -
      4. -
      -
      -
    • -
    +
      + + + + diff --git a/js/constant.js b/js/constant.js new file mode 100644 index 0000000..d01d417 --- /dev/null +++ b/js/constant.js @@ -0,0 +1,2 @@ +const OPEN = 'open'; +const DONE = 'done'; diff --git a/js/index.js b/js/index.js index 5029ab4..bd1c0fd 100644 --- a/js/index.js +++ b/js/index.js @@ -1,12 +1,17 @@ document.addEventListener('DOMContentLoaded', () => { const subjectViewModel = new SubjectViewModel(); - const openSubject1 = new Subject('Opened Subject 1'); + const openSubject1 = new Subject('Opened Subject 1', OPEN); openSubject1.addTask(new Task('task 1')); openSubject1.addTask(new Task('task 2')); - subjectViewModel.addCard(openSubject1); + subjectViewModel.addSubject(openSubject1); - const openSubject2 = new Subject('Opened Subject 2'); + const openSubject2 = new Subject('Opened Subject 2', OPEN); openSubject2.addTask(new Task('task 1')); - subjectViewModel.addCard(openSubject2); + subjectViewModel.addSubject(openSubject2); + + const doneSubject1 = new Subject('Done Subject 1', DONE); + doneSubject1.addTask(new Task('task 1')); + doneSubject1.addTask(new Task('task 2')); + subjectViewModel.addSubject(doneSubject1); }); diff --git a/js/models/subject.js b/js/models/subject.js index 8ccf6f8..5c81b31 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -1,10 +1,19 @@ class Subject { - constructor(title = 'New Subject') { + constructor(title = 'New Subject', state = OPEN) { this.title = title; this.taskList = []; + this.state = state; } addTask(task) { this.taskList.push(task); } + + getTitle() { + return this.title; + } + + getState() { + return this.state; + } } diff --git a/js/models/task.js b/js/models/task.js index d2e57b7..6eed485 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -2,6 +2,4 @@ class Task { constructor(title) { this.title = title; } - - render() {} } diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index a0bd6a9..2709a7f 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -9,6 +9,30 @@ class SubjectViewModel { } render() { - // render 로직 + const columnList = [OPEN, DONE]; + + columnList.forEach((column) => { + const subjectListElement = document.getElementById( + `${column}-subject-list` + ); + subjectListElement.innerHTML = ''; + + this.subjectList.forEach((subject) => { + if (subject.getState() !== column) return; + + const subjectElement = document.createElement('li'); + subjectElement.classList.add('card'); + subjectElement.innerHTML = ` +
      +

      ${subject.getTitle()}

      +
      +
      +
        +
      +
      + `; + subjectListElement.appendChild(subjectElement); + }); + }); } } From 5e17bce280aea2049a44989be50ddddfef90ecb3 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 22:47:36 +0900 Subject: [PATCH 09/57] feat: render subject tasks --- index.html | 1 + js/index.js | 13 +++++++------ js/models/subject.js | 9 +++++++++ js/models/task.js | 7 ++++++- js/viewModels/subjectViewModel.js | 25 +++++++++++++++++++++++-- js/viewModels/taskViewModel.js | 17 +++++++++++++++++ 6 files changed, 63 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 41fab25..bfcfe3a 100644 --- a/index.html +++ b/index.html @@ -40,5 +40,6 @@

      Done

      + diff --git a/js/index.js b/js/index.js index bd1c0fd..273f474 100644 --- a/js/index.js +++ b/js/index.js @@ -1,17 +1,18 @@ document.addEventListener('DOMContentLoaded', () => { - const subjectViewModel = new SubjectViewModel(); + const taskViewModel = new TaskViewModel(); + const subjectViewModel = new SubjectViewModel(taskViewModel); const openSubject1 = new Subject('Opened Subject 1', OPEN); - openSubject1.addTask(new Task('task 1')); - openSubject1.addTask(new Task('task 2')); + taskViewModel.addTask('task 1', openSubject1.getId()); + taskViewModel.addTask('task 2', openSubject1.getId()); subjectViewModel.addSubject(openSubject1); const openSubject2 = new Subject('Opened Subject 2', OPEN); - openSubject2.addTask(new Task('task 1')); + taskViewModel.addTask('task 1', openSubject2.getId()); subjectViewModel.addSubject(openSubject2); const doneSubject1 = new Subject('Done Subject 1', DONE); - doneSubject1.addTask(new Task('task 1')); - doneSubject1.addTask(new Task('task 2')); + taskViewModel.addTask('task 1', doneSubject1.getId()); + taskViewModel.addTask('task 2', doneSubject1.getId()); subjectViewModel.addSubject(doneSubject1); }); diff --git a/js/models/subject.js b/js/models/subject.js index 5c81b31..23efc1c 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -3,6 +3,7 @@ class Subject { this.title = title; this.taskList = []; this.state = state; + this.id = Math.random().toString(36).substring(2); // [todo] make random id function } addTask(task) { @@ -16,4 +17,12 @@ class Subject { getState() { return this.state; } + + getTaskList() { + return this.taskList; + } + + getId() { + return this.id; + } } diff --git a/js/models/task.js b/js/models/task.js index 6eed485..ea37578 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -1,5 +1,10 @@ class Task { - constructor(title) { + constructor(title, subjectId) { this.title = title; + this.subjectId = subjectId; + } + + getTitle() { + return this.title; } } diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 2709a7f..89f1ee9 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -1,6 +1,7 @@ class SubjectViewModel { - constructor() { + constructor(taskViewModel) { this.subjectList = []; + this.taskViewModel = taskViewModel; } addSubject(subject) { @@ -18,6 +19,8 @@ class SubjectViewModel { subjectListElement.innerHTML = ''; this.subjectList.forEach((subject) => { + const subjectId = subject.getId(); + const taskListElementId = `${subjectId}-task-list`; if (subject.getState() !== column) return; const subjectElement = document.createElement('li'); @@ -27,12 +30,30 @@ class SubjectViewModel {

      ${subject.getTitle()}

      -
        +
      `; subjectListElement.appendChild(subjectElement); + + const taskListElement = document.getElementById(taskListElementId); + this.renderTasks(subjectId, taskListElement); }); }); } + + renderTasks(subjectId, taskListElement) { + this.taskViewModel.getTasksBySubject(subjectId).forEach((task) => { + const taskElement = document.createElement('li'); + taskElement.className = 'task'; + const checkboxElement = document.createElement('input'); + checkboxElement.type = 'checkbox'; + const titleElement = document.createElement('p'); + titleElement.innerText = task.getTitle(); + + taskElement.appendChild(checkboxElement); + taskElement.appendChild(titleElement); + taskListElement.appendChild(taskElement); + }); + } } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index e69de29..48b2a2b 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -0,0 +1,17 @@ +class TaskViewModel { + constructor() { + this.taskList = new Map(); + } + + addTask(title, subjectId) { + const task = new Task(title, subjectId); + if (!this.taskList.has(subjectId)) { + this.taskList.set(subjectId, []); + } + this.taskList.get(subjectId).push(task); + } + + getTasksBySubject(subjectId) { + return this.taskList.get(subjectId) || []; + } +} From 7c45c22412c98c19afc7ee9075f50b6c8f7c47d1 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 22:48:53 +0900 Subject: [PATCH 10/57] docs: edit terms --- todo.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/todo.md b/todo.md index c35f4fa..34bb4ce 100644 --- a/todo.md +++ b/todo.md @@ -4,14 +4,14 @@ | -------- | ---------------------------------- | ----------------------------------- | | high | **기본 레이아웃** | 헤더, column(open, done) 배치 | | | | CSS 스타일 적용 | -| high | **데이터 구조 작성** | 카드 및 태스크 class 정의 | -| high | **카드 추가** | 카드 추가 버튼과 모달 구현 | -| | | 카드 추가 버튼 이벤트 핸들러 작성 | -| high | **카드 삭제** | 카드 삭제 버튼 구현 | -| | | 카드 삭제 버튼 이벤트 핸들러 작성 | -| high | **카드 상태 토글** | 카드 완료 체크박스 추가 | -| | | 카드 상태에 따른 스타일 변경 | -| | | 카드 상태 변경 이벤트 핸들러 작성 | +| high | **데이터 구조 작성** | 목표 및 태스크 class 정의 | +| high | **목표 추가** | 목표 추가 버튼과 모달 구현 | +| | | 목표 추가 버튼 이벤트 핸들러 작성 | +| high | **목표 삭제** | 목표 삭제 버튼 구현 | +| | | 목표 삭제 버튼 이벤트 핸들러 작성 | +| high | **목표 상태 토글** | 목표 완료 체크박스 추가 | +| | | 목표 상태에 따른 스타일 변경 | +| | | 목표 상태 변경 이벤트 핸들러 작성 | | high | **태스크 추가** | 태스크 input 필드 구현 | | | | submit 이벤트 핸들러 작성 | | high | **태스크 삭제** | 태스크 삭제 버튼 구현 | From 7f855720ce7b4c9273c695f49e05511b3f1abb1c Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 23:22:19 +0900 Subject: [PATCH 11/57] feat: make add subject button --- assets/addIcon.svg | 5 +++++ css/card.css | 14 ++++++++++++++ css/layout.css | 5 +++++ index.html | 14 ++++++++++++-- 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 assets/addIcon.svg diff --git a/assets/addIcon.svg b/assets/addIcon.svg new file mode 100644 index 0000000..9ced21e --- /dev/null +++ b/assets/addIcon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/css/card.css b/css/card.css index d1d3d2a..2c4f3b4 100644 --- a/css/card.css +++ b/css/card.css @@ -12,3 +12,17 @@ border: 2px solid var(--card-border); border-radius: 1rem; } + +.add-subject-button { + background: transparent; + + border: none; + border-radius: 100%; + outline: none; + + cursor: pointer; +} +.add-subject-button > img { + width: 2rem; + height: 2rem; +} diff --git a/css/layout.css b/css/layout.css index 5ea7a0b..f21b0b2 100644 --- a/css/layout.css +++ b/css/layout.css @@ -52,6 +52,11 @@ header > h1 { border-color: var(--done-column-border); } +.board-column-header { + display: flex; + justify-content: space-between; +} + .open-hr { border: 1px solid var(--open-column-border); } diff --git a/index.html b/index.html index bfcfe3a..302eae5 100644 --- a/index.html +++ b/index.html @@ -19,7 +19,12 @@

      20XX. XX. XX.

      -

      Open

      +
      +

      Open

      + +

      @@ -27,7 +32,12 @@

      Open

      -

      Done

      +
      +

      Done

      + +

      From 85242f176bfd5b2d174c0f21965110da47c90de5 Mon Sep 17 00:00:00 2001 From: psst54 Date: Thu, 5 Sep 2024 23:23:14 +0900 Subject: [PATCH 12/57] chore: rename component --- css/{card.css => subject.css} | 8 ++++---- css/variables.css | 4 ++-- index.html | 6 +++--- js/viewModels/subjectViewModel.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) rename css/{card.css => subject.css} (72%) diff --git a/css/card.css b/css/subject.css similarity index 72% rename from css/card.css rename to css/subject.css index 2c4f3b4..372fa52 100644 --- a/css/card.css +++ b/css/subject.css @@ -1,15 +1,15 @@ -.card-list { +.subject-list { display: flex; flex-direction: column; gap: 1rem; } -.card { +.subject { padding: 1rem; - background: var(--card-background); + background: var(--subject-background); - border: 2px solid var(--card-border); + border: 2px solid var(--subject-border); border-radius: 1rem; } diff --git a/css/variables.css b/css/variables.css index 9c4bf15..5ca6996 100644 --- a/css/variables.css +++ b/css/variables.css @@ -6,8 +6,8 @@ --open-column-border: #0d5580; --done-column-border: #0b5c18; - --card-background: #121212ee; - --card-border: #505050; + --subject-background: #121212ee; + --subject-border: #505050; --text-color: #ffffff; } diff --git a/index.html b/index.html index 302eae5..e87cf0c 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Vanilla Todo - + @@ -28,7 +28,7 @@

      Open


      -
        +
          @@ -41,7 +41,7 @@

          Done


          -
            +
              diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 89f1ee9..24968e6 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -24,7 +24,7 @@ class SubjectViewModel { if (subject.getState() !== column) return; const subjectElement = document.createElement('li'); - subjectElement.classList.add('card'); + subjectElement.classList.add('subject'); subjectElement.innerHTML = `

              ${subject.getTitle()}

              From 16f8fd7566a96273e89eb7ded92f12bdcce036f5 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 00:12:51 +0900 Subject: [PATCH 13/57] feat: add new subject --- index.html | 4 ++-- js/constant.js | 1 + js/index.js | 20 ++++++++++++++------ js/viewModels/subjectViewModel.js | 31 +++++++------------------------ js/viewModels/taskViewModel.js | 21 +++++++++++++++++++++ 5 files changed, 45 insertions(+), 32 deletions(-) diff --git a/index.html b/index.html index e87cf0c..2a09721 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@

              20XX. XX. XX.

              Open

              -
              @@ -34,7 +34,7 @@

              Open

              Done

              -
              diff --git a/js/constant.js b/js/constant.js index d01d417..f2a8b7a 100644 --- a/js/constant.js +++ b/js/constant.js @@ -1,2 +1,3 @@ const OPEN = 'open'; const DONE = 'done'; +const COLUMN_LIST = [OPEN, DONE]; diff --git a/js/index.js b/js/index.js index 273f474..1d7e0bb 100644 --- a/js/index.js +++ b/js/index.js @@ -2,17 +2,25 @@ document.addEventListener('DOMContentLoaded', () => { const taskViewModel = new TaskViewModel(); const subjectViewModel = new SubjectViewModel(taskViewModel); - const openSubject1 = new Subject('Opened Subject 1', OPEN); + const openSubject1 = subjectViewModel.addSubject('Opened Subject 1', OPEN); taskViewModel.addTask('task 1', openSubject1.getId()); taskViewModel.addTask('task 2', openSubject1.getId()); - subjectViewModel.addSubject(openSubject1); - const openSubject2 = new Subject('Opened Subject 2', OPEN); + const openSubject2 = subjectViewModel.addSubject('Opened Subject 2', OPEN); taskViewModel.addTask('task 1', openSubject2.getId()); - subjectViewModel.addSubject(openSubject2); - const doneSubject1 = new Subject('Done Subject 1', DONE); + const doneSubject1 = subjectViewModel.addSubject('Done Subject 1', DONE); taskViewModel.addTask('task 1', doneSubject1.getId()); taskViewModel.addTask('task 2', doneSubject1.getId()); - subjectViewModel.addSubject(doneSubject1); + + COLUMN_LIST.forEach((column) => { + const addSubjectButtonElement = document.getElementById( + `add-subject-button-${column.toLowerCase()}` + ); + + addSubjectButtonElement.addEventListener('click', () => { + const subjectName = 'New Subject'; + subjectViewModel.addSubject(subjectName, column); + }); + }); }); diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 24968e6..83a4959 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -4,15 +4,16 @@ class SubjectViewModel { this.taskViewModel = taskViewModel; } - addSubject(subject) { + addSubject(title, state = OPEN) { + const subject = new Subject(title, state); this.subjectList.push(subject); + this.render(); + return subject; } render() { - const columnList = [OPEN, DONE]; - - columnList.forEach((column) => { + COLUMN_LIST.forEach((column) => { const subjectListElement = document.getElementById( `${column}-subject-list` ); @@ -26,9 +27,7 @@ class SubjectViewModel { const subjectElement = document.createElement('li'); subjectElement.classList.add('subject'); subjectElement.innerHTML = ` -
              -

              ${subject.getTitle()}

              -
              +

              ${subject.getTitle()}

              @@ -36,24 +35,8 @@ class SubjectViewModel { `; subjectListElement.appendChild(subjectElement); - const taskListElement = document.getElementById(taskListElementId); - this.renderTasks(subjectId, taskListElement); + this.taskViewModel.render(subjectId); }); }); } - - renderTasks(subjectId, taskListElement) { - this.taskViewModel.getTasksBySubject(subjectId).forEach((task) => { - const taskElement = document.createElement('li'); - taskElement.className = 'task'; - const checkboxElement = document.createElement('input'); - checkboxElement.type = 'checkbox'; - const titleElement = document.createElement('p'); - titleElement.innerText = task.getTitle(); - - taskElement.appendChild(checkboxElement); - taskElement.appendChild(titleElement); - taskListElement.appendChild(taskElement); - }); - } } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 48b2a2b..3c439e6 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -9,9 +9,30 @@ class TaskViewModel { this.taskList.set(subjectId, []); } this.taskList.get(subjectId).push(task); + + this.render(subjectId); } getTasksBySubject(subjectId) { return this.taskList.get(subjectId) || []; } + + render(subjectId) { + const taskListElementId = `${subjectId}-task-list`; + const taskListElement = document.getElementById(taskListElementId); + taskListElement.innerHTML = ''; + + this.getTasksBySubject(subjectId).forEach((task) => { + const taskElement = document.createElement('li'); + taskElement.className = 'task'; + const checkboxElement = document.createElement('input'); + checkboxElement.type = 'checkbox'; + const titleElement = document.createElement('p'); + titleElement.innerText = task.getTitle(); + + taskElement.appendChild(checkboxElement); + taskElement.appendChild(titleElement); + taskListElement.appendChild(taskElement); + }); + } } From a8ce087253a769facd170da16563a53dc85fa48b Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 00:21:56 +0900 Subject: [PATCH 14/57] feat: add in-progress column --- css/layout.css | 9 +++++++++ css/variables.css | 2 ++ index.html | 16 ++++++++++++++++ js/constant.js | 3 ++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/css/layout.css b/css/layout.css index f21b0b2..0624ffd 100644 --- a/css/layout.css +++ b/css/layout.css @@ -47,6 +47,11 @@ header > h1 { border-color: var(--open-column-border); } +#in-progress-column { + background-color: var(--in-progress-column-background); + border-color: var(--in-progress-column-border); +} + #done-column { background-color: var(--done-column-background); border-color: var(--done-column-border); @@ -61,6 +66,10 @@ header > h1 { border: 1px solid var(--open-column-border); } +.in-progress-hr { + border: 1px solid var(--in-progress-column-border); +} + .done-hr { border: 1px solid var(--done-column-border); } diff --git a/css/variables.css b/css/variables.css index 5ca6996..8eb4326 100644 --- a/css/variables.css +++ b/css/variables.css @@ -2,8 +2,10 @@ --background: #121212; --open-column-background: #31627e1a; + --in-progress-column-background: #7267451a; --done-column-background: #2a5f2d1a; --open-column-border: #0d5580; + --in-progress-column-border: #785e17; --done-column-border: #0b5c18; --subject-background: #121212ee; diff --git a/index.html b/index.html index 2a09721..a4ae13d 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,22 @@

              Open

                +
                +
                +

                In Progress

                + +
                + +
                + +
                  +
                  +

                  Done

                  diff --git a/js/constant.js b/js/constant.js index f2a8b7a..ae3b1c2 100644 --- a/js/constant.js +++ b/js/constant.js @@ -1,3 +1,4 @@ const OPEN = 'open'; +const IN_PROGRESS = 'in-progress'; const DONE = 'done'; -const COLUMN_LIST = [OPEN, DONE]; +const COLUMN_LIST = [OPEN, IN_PROGRESS, DONE]; From d9542b3b9fbdbb648f039fcd6fd7f24deb0de250 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 00:31:05 +0900 Subject: [PATCH 15/57] refactor: change subjectList to map --- js/viewModels/subjectViewModel.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 83a4959..db2f0c6 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -1,17 +1,24 @@ class SubjectViewModel { constructor(taskViewModel) { - this.subjectList = []; + this.subjectList = new Map(); this.taskViewModel = taskViewModel; } addSubject(title, state = OPEN) { const subject = new Subject(title, state); - this.subjectList.push(subject); + if (!this.subjectList.has(state)) { + this.subjectList.set(state, []); + } + this.subjectList.get(state).push(subject); this.render(); return subject; } + getSubjectsByColumn(columnId) { + return this.subjectList.get(columnId) || []; + } + render() { COLUMN_LIST.forEach((column) => { const subjectListElement = document.getElementById( @@ -19,18 +26,18 @@ class SubjectViewModel { ); subjectListElement.innerHTML = ''; - this.subjectList.forEach((subject) => { + this.getSubjectsByColumn(column).forEach((subject) => { const subjectId = subject.getId(); - const taskListElementId = `${subjectId}-task-list`; - if (subject.getState() !== column) return; + if (subject.getState() !== column) { + return; + } const subjectElement = document.createElement('li'); subjectElement.classList.add('subject'); subjectElement.innerHTML = `

                  ${subject.getTitle()}

                  -
                    -
                  +
                  `; subjectListElement.appendChild(subjectElement); From 8f289c29cfc6e2ca667269a746d5cd337758f772 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 00:45:24 +0900 Subject: [PATCH 16/57] feat: make remove button --- assets/deleteIcon.svg | 2 ++ css/subject.css | 18 ++++++++++++++++++ js/viewModels/subjectViewModel.js | 7 ++++++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 assets/deleteIcon.svg diff --git a/assets/deleteIcon.svg b/assets/deleteIcon.svg new file mode 100644 index 0000000..e287d7f --- /dev/null +++ b/assets/deleteIcon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/css/subject.css b/css/subject.css index 372fa52..f14d4a5 100644 --- a/css/subject.css +++ b/css/subject.css @@ -13,6 +13,11 @@ border-radius: 1rem; } +.subject-header { + display: flex; + justify-content: space-between; +} + .add-subject-button { background: transparent; @@ -26,3 +31,16 @@ width: 2rem; height: 2rem; } + +.delete-subject-button { + background: transparent; + + border: none; + outline: none; + + cursor: pointer; +} +.delete-subject-icon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index db2f0c6..2a601bf 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -35,7 +35,12 @@ class SubjectViewModel { const subjectElement = document.createElement('li'); subjectElement.classList.add('subject'); subjectElement.innerHTML = ` -

                  ${subject.getTitle()}

                  +
                  +

                  ${subject.getTitle()}

                  + +
                  From 7cf1077900661c4850e423051334148bf018e8c7 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 13:51:01 +0900 Subject: [PATCH 17/57] feat: remove subject --- js/viewModels/subjectViewModel.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 2a601bf..6f69c4f 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -15,6 +15,16 @@ class SubjectViewModel { return subject; } + deleteSubject(subjectId, state) { + const subjects = this.subjectList.get(state); + const subjectIndex = subjects.findIndex( + (subject) => subject.getId() === subjectId + ); + subjects.splice(subjectIndex, 1); + + this.render(); + } + getSubjectsByColumn(columnId) { return this.subjectList.get(columnId) || []; } @@ -37,7 +47,7 @@ class SubjectViewModel { subjectElement.innerHTML = `

                  ${subject.getTitle()}

                  -
                  @@ -47,6 +57,13 @@ class SubjectViewModel { `; subjectListElement.appendChild(subjectElement); + const deleteButtonElement = document.getElementById( + `${subjectId}-delete-button` + ); + deleteButtonElement.addEventListener('click', () => + this.deleteSubject(subjectId, column) + ); + this.taskViewModel.render(subjectId); }); }); From b0c5e6a2e12e37b5c9d0720fb795c95b8ae0bce1 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 13:55:35 +0900 Subject: [PATCH 18/57] chore: rename variable for clarity --- js/constant.js | 2 +- js/index.js | 6 +++--- js/viewModels/subjectViewModel.js | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/js/constant.js b/js/constant.js index ae3b1c2..b18a6b8 100644 --- a/js/constant.js +++ b/js/constant.js @@ -1,4 +1,4 @@ const OPEN = 'open'; const IN_PROGRESS = 'in-progress'; const DONE = 'done'; -const COLUMN_LIST = [OPEN, IN_PROGRESS, DONE]; +const STATE_LIST = [OPEN, IN_PROGRESS, DONE]; diff --git a/js/index.js b/js/index.js index 1d7e0bb..89b5b45 100644 --- a/js/index.js +++ b/js/index.js @@ -13,14 +13,14 @@ document.addEventListener('DOMContentLoaded', () => { taskViewModel.addTask('task 1', doneSubject1.getId()); taskViewModel.addTask('task 2', doneSubject1.getId()); - COLUMN_LIST.forEach((column) => { + STATE_LIST.forEach((state) => { const addSubjectButtonElement = document.getElementById( - `add-subject-button-${column.toLowerCase()}` + `add-subject-button-${state.toLowerCase()}` ); addSubjectButtonElement.addEventListener('click', () => { const subjectName = 'New Subject'; - subjectViewModel.addSubject(subjectName, column); + subjectViewModel.addSubject(subjectName, state); }); }); }); diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 6f69c4f..44e8688 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -25,20 +25,20 @@ class SubjectViewModel { this.render(); } - getSubjectsByColumn(columnId) { - return this.subjectList.get(columnId) || []; + getSubjectsByState(state) { + return this.subjectList.get(state) || []; } render() { - COLUMN_LIST.forEach((column) => { + STATE_LIST.forEach((state) => { const subjectListElement = document.getElementById( - `${column}-subject-list` + `${state}-subject-list` ); subjectListElement.innerHTML = ''; - this.getSubjectsByColumn(column).forEach((subject) => { + this.getSubjectsByState(state).forEach((subject) => { const subjectId = subject.getId(); - if (subject.getState() !== column) { + if (subject.getState() !== state) { return; } @@ -61,7 +61,7 @@ class SubjectViewModel { `${subjectId}-delete-button` ); deleteButtonElement.addEventListener('click', () => - this.deleteSubject(subjectId, column) + this.deleteSubject(subjectId, state) ); this.taskViewModel.render(subjectId); From 550492549c057892515afbc8b0c4e6aa13407307 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:06:11 +0900 Subject: [PATCH 19/57] chore: set class properties to private --- js/models/subject.js | 23 ++++++++++++++--------- js/models/task.js | 9 ++++++--- js/viewModels/subjectViewModel.js | 19 +++++++++++-------- js/viewModels/taskViewModel.js | 12 +++++++----- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/js/models/subject.js b/js/models/subject.js index 23efc1c..b116a14 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -1,28 +1,33 @@ class Subject { + #title; + #taskList; + #state; + #id; + constructor(title = 'New Subject', state = OPEN) { - this.title = title; - this.taskList = []; - this.state = state; - this.id = Math.random().toString(36).substring(2); // [todo] make random id function + this.#title = title; + this.#taskList = []; + this.#state = state; + this.#id = Math.random().toString(36).substring(2); // [todo] make random id function } addTask(task) { - this.taskList.push(task); + this.#taskList.push(task); } getTitle() { - return this.title; + return this.#title; } getState() { - return this.state; + return this.#state; } getTaskList() { - return this.taskList; + return this.#taskList; } getId() { - return this.id; + return this.#id; } } diff --git a/js/models/task.js b/js/models/task.js index ea37578..76fbaa5 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -1,10 +1,13 @@ class Task { + #title; + #subjectId; + constructor(title, subjectId) { - this.title = title; - this.subjectId = subjectId; + this.#title = title; + this.#subjectId = subjectId; } getTitle() { - return this.title; + return this.#title; } } diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 44e8688..ff25d45 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -1,22 +1,25 @@ class SubjectViewModel { + #subjectList; + #taskViewModel; + constructor(taskViewModel) { - this.subjectList = new Map(); - this.taskViewModel = taskViewModel; + this.#subjectList = new Map(); + this.#taskViewModel = taskViewModel; } addSubject(title, state = OPEN) { const subject = new Subject(title, state); - if (!this.subjectList.has(state)) { - this.subjectList.set(state, []); + if (!this.#subjectList.has(state)) { + this.#subjectList.set(state, []); } - this.subjectList.get(state).push(subject); + this.#subjectList.get(state).push(subject); this.render(); return subject; } deleteSubject(subjectId, state) { - const subjects = this.subjectList.get(state); + const subjects = this.#subjectList.get(state); const subjectIndex = subjects.findIndex( (subject) => subject.getId() === subjectId ); @@ -26,7 +29,7 @@ class SubjectViewModel { } getSubjectsByState(state) { - return this.subjectList.get(state) || []; + return this.#subjectList.get(state) || []; } render() { @@ -64,7 +67,7 @@ class SubjectViewModel { this.deleteSubject(subjectId, state) ); - this.taskViewModel.render(subjectId); + this.#taskViewModel.render(subjectId); }); }); } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 3c439e6..4de6f60 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -1,20 +1,22 @@ class TaskViewModel { + #taskList; + constructor() { - this.taskList = new Map(); + this.#taskList = new Map(); } addTask(title, subjectId) { const task = new Task(title, subjectId); - if (!this.taskList.has(subjectId)) { - this.taskList.set(subjectId, []); + if (!this.#taskList.has(subjectId)) { + this.#taskList.set(subjectId, []); } - this.taskList.get(subjectId).push(task); + this.#taskList.get(subjectId).push(task); this.render(subjectId); } getTasksBySubject(subjectId) { - return this.taskList.get(subjectId) || []; + return this.#taskList.get(subjectId) || []; } render(subjectId) { From 2d4a7962740d21f1bbec8dda08c03a49bf072786 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:08:23 +0900 Subject: [PATCH 20/57] chore: change variable names for clarity --- js/viewModels/subjectViewModel.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index ff25d45..a909f5b 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -18,12 +18,12 @@ class SubjectViewModel { return subject; } - deleteSubject(subjectId, state) { - const subjects = this.#subjectList.get(state); - const subjectIndex = subjects.findIndex( - (subject) => subject.getId() === subjectId + deleteSubject(targetId, state) { + const subjectList = this.#subjectList.get(state); + const targetIndex = subjectList.findIndex( + (subject) => subject.getId() === targetId ); - subjects.splice(subjectIndex, 1); + subjectList.splice(targetIndex, 1); this.render(); } From a654df19e14f27c447f455e1506ec18c3d53ea64 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:09:31 +0900 Subject: [PATCH 21/57] chore: set class methods to private --- js/viewModels/subjectViewModel.js | 4 ++-- js/viewModels/taskViewModel.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index a909f5b..a38595a 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -28,7 +28,7 @@ class SubjectViewModel { this.render(); } - getSubjectsByState(state) { + #getSubjectsByState(state) { return this.#subjectList.get(state) || []; } @@ -39,7 +39,7 @@ class SubjectViewModel { ); subjectListElement.innerHTML = ''; - this.getSubjectsByState(state).forEach((subject) => { + this.#getSubjectsByState(state).forEach((subject) => { const subjectId = subject.getId(); if (subject.getState() !== state) { return; diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 4de6f60..7941c9c 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -15,7 +15,7 @@ class TaskViewModel { this.render(subjectId); } - getTasksBySubject(subjectId) { + #getTasksBySubject(subjectId) { return this.#taskList.get(subjectId) || []; } @@ -24,7 +24,7 @@ class TaskViewModel { const taskListElement = document.getElementById(taskListElementId); taskListElement.innerHTML = ''; - this.getTasksBySubject(subjectId).forEach((task) => { + this.#getTasksBySubject(subjectId).forEach((task) => { const taskElement = document.createElement('li'); taskElement.className = 'task'; const checkboxElement = document.createElement('input'); From b68de2604383df507fde2e676ea179d094a5adbf Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:11:46 +0900 Subject: [PATCH 22/57] chore: add constant string --- js/constant.js | 2 ++ js/index.js | 2 +- js/models/subject.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/constant.js b/js/constant.js index b18a6b8..3d4408f 100644 --- a/js/constant.js +++ b/js/constant.js @@ -2,3 +2,5 @@ const OPEN = 'open'; const IN_PROGRESS = 'in-progress'; const DONE = 'done'; const STATE_LIST = [OPEN, IN_PROGRESS, DONE]; + +const NEW_SUBJECT_NAME = 'New Subject'; diff --git a/js/index.js b/js/index.js index 89b5b45..74fbe8f 100644 --- a/js/index.js +++ b/js/index.js @@ -19,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => { ); addSubjectButtonElement.addEventListener('click', () => { - const subjectName = 'New Subject'; + const subjectName = NEW_SUBJECT_NAME; // [todo] remove this line subjectViewModel.addSubject(subjectName, state); }); }); diff --git a/js/models/subject.js b/js/models/subject.js index b116a14..06afc3f 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -4,7 +4,7 @@ class Subject { #state; #id; - constructor(title = 'New Subject', state = OPEN) { + constructor(title = NEW_SUBJECT_NAME, state = OPEN) { this.#title = title; this.#taskList = []; this.#state = state; From bf526f75352ced98edcad27c60e8b1ea74cb077f Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:14:51 +0900 Subject: [PATCH 23/57] dcos: add comments --- js/index.js | 5 +++++ js/models/subject.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index 74fbe8f..ae9678d 100644 --- a/js/index.js +++ b/js/index.js @@ -2,6 +2,9 @@ document.addEventListener('DOMContentLoaded', () => { const taskViewModel = new TaskViewModel(); const subjectViewModel = new SubjectViewModel(taskViewModel); + // [todo] remove below + // ------------------------------------------------- + const openSubject1 = subjectViewModel.addSubject('Opened Subject 1', OPEN); taskViewModel.addTask('task 1', openSubject1.getId()); taskViewModel.addTask('task 2', openSubject1.getId()); @@ -13,6 +16,8 @@ document.addEventListener('DOMContentLoaded', () => { taskViewModel.addTask('task 1', doneSubject1.getId()); taskViewModel.addTask('task 2', doneSubject1.getId()); + // -------------------------------------------------- + STATE_LIST.forEach((state) => { const addSubjectButtonElement = document.getElementById( `add-subject-button-${state.toLowerCase()}` diff --git a/js/models/subject.js b/js/models/subject.js index 06afc3f..142c1ab 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -6,7 +6,7 @@ class Subject { constructor(title = NEW_SUBJECT_NAME, state = OPEN) { this.#title = title; - this.#taskList = []; + this.#taskList = []; // [question] is this necessary? this.#state = state; this.#id = Math.random().toString(36).substring(2); // [todo] make random id function } From 0d03723497cd6da41bb8b0808490abcd7c01c7f9 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:36:31 +0900 Subject: [PATCH 24/57] feat: add task input form --- css/task.css | 30 ++++++++++++++++++++++++++++++ css/variables.css | 2 ++ js/viewModels/taskViewModel.js | 21 +++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/css/task.css b/css/task.css index 89a723f..84c767f 100644 --- a/css/task.css +++ b/css/task.css @@ -3,3 +3,33 @@ align-items: center; gap: 0.5rem; } + +.add-task-form { + display: flex; + gap: 0.5rem; + + width: 100%; +} + +.add-task-form > input { + flex: 1; + + background: transparent; + + border: none; + border-bottom: 1px solid var(--border); + outline: none; + + color: var(--text-color); +} +.add-task-form > button { + padding: 0.25rem 0.5rem; + background: transparent; + + border: 1px solid white; + border-radius: 1rem; + outline: none; + + color: var(--text-color); + white-space: nowrap; +} diff --git a/css/variables.css b/css/variables.css index 8eb4326..2c903d9 100644 --- a/css/variables.css +++ b/css/variables.css @@ -12,4 +12,6 @@ --subject-border: #505050; --text-color: #ffffff; + + --border: #505050; } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 7941c9c..674f8f5 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -19,11 +19,32 @@ class TaskViewModel { return this.#taskList.get(subjectId) || []; } + #createTaskInputElement(subjectId) { + const formId = `${subjectId}-add-task-form`; + const formElement = document.createElement('form'); + formElement.id = formId; + formElement.classList.add('add-task-form'); + + const inputElement = document.createElement('input'); + inputElement.type = 'text'; + inputElement.placeholder = '할 일을 입력해주세요'; + + const submitButtonElement = document.createElement('button'); + submitButtonElement.innerText = '추가'; + + formElement.appendChild(inputElement); + formElement.appendChild(submitButtonElement); + + return formElement; + } + render(subjectId) { const taskListElementId = `${subjectId}-task-list`; const taskListElement = document.getElementById(taskListElementId); taskListElement.innerHTML = ''; + taskListElement.appendChild(this.#createTaskInputElement(subjectId)); + this.#getTasksBySubject(subjectId).forEach((task) => { const taskElement = document.createElement('li'); taskElement.className = 'task'; From a4da6c590f80e5fa0a9253080faeb5cd8b5aa3bf Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:38:02 +0900 Subject: [PATCH 25/57] feat: add submit event handler to add task --- js/viewModels/taskViewModel.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 674f8f5..90c3be4 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -35,6 +35,14 @@ class TaskViewModel { formElement.appendChild(inputElement); formElement.appendChild(submitButtonElement); + formElement.addEventListener('submit', (event) => { + event.preventDefault(); + if (!inputElement.value) { + return; + } + this.addTask(inputElement.value, subjectId); + }); + return formElement; } From f98becd0d6e0f4168a7a47a83f18d472e396bc3e Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:42:27 +0900 Subject: [PATCH 26/57] feat: fix task style --- css/subject.css | 2 +- css/task.css | 10 +++++++++- css/variables.css | 2 +- js/viewModels/subjectViewModel.js | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/css/subject.css b/css/subject.css index f14d4a5..41717fc 100644 --- a/css/subject.css +++ b/css/subject.css @@ -9,7 +9,7 @@ background: var(--subject-background); - border: 2px solid var(--subject-border); + border: 2px solid var(--border); border-radius: 1rem; } diff --git a/css/task.css b/css/task.css index 84c767f..20e2ce9 100644 --- a/css/task.css +++ b/css/task.css @@ -1,3 +1,9 @@ +.task-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + .task { display: flex; align-items: center; @@ -26,10 +32,12 @@ padding: 0.25rem 0.5rem; background: transparent; - border: 1px solid white; + border: 1px solid var(--button-border); border-radius: 1rem; outline: none; color: var(--text-color); white-space: nowrap; + + cursor: pointer; } diff --git a/css/variables.css b/css/variables.css index 2c903d9..c240e4c 100644 --- a/css/variables.css +++ b/css/variables.css @@ -9,9 +9,9 @@ --done-column-border: #0b5c18; --subject-background: #121212ee; - --subject-border: #505050; --text-color: #ffffff; --border: #505050; + --button-border: #ffffff; } diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index a38595a..36fa1c1 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -55,7 +55,7 @@ class SubjectViewModel {
                  -
                    +
                  `; subjectListElement.appendChild(subjectElement); From f644949b1071b7b1f6265b70e323f401a846bdcd Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 14:46:16 +0900 Subject: [PATCH 27/57] refactor: change method --- js/viewModels/taskViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 90c3be4..166d66e 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -55,7 +55,7 @@ class TaskViewModel { this.#getTasksBySubject(subjectId).forEach((task) => { const taskElement = document.createElement('li'); - taskElement.className = 'task'; + taskElement.classList.add('task'); const checkboxElement = document.createElement('input'); checkboxElement.type = 'checkbox'; const titleElement = document.createElement('p'); From c4c4478575cda526b41b8b3a2ce24045399e9ca8 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 15:10:15 +0900 Subject: [PATCH 28/57] refactor: make createElement function --- index.html | 1 + js/utils/index.js | 11 +++++++++++ js/viewModels/taskViewModel.js | 25 ++++++++++++------------- 3 files changed, 24 insertions(+), 13 deletions(-) create mode 100644 js/utils/index.js diff --git a/index.html b/index.html index a4ae13d..31a3b93 100644 --- a/index.html +++ b/index.html @@ -68,4 +68,5 @@

                  Done

                  + diff --git a/js/utils/index.js b/js/utils/index.js new file mode 100644 index 0000000..df5f273 --- /dev/null +++ b/js/utils/index.js @@ -0,0 +1,11 @@ +function createElement(tag, attributes = {}) { + const element = document.createElement(tag); + Object.entries(attributes).forEach(([key, value]) => { + // if (key in element) { + // element[key] = value; + // } else { + element.setAttribute(key, value); + // } + }); + return element; +} diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 166d66e..3537f4a 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -20,20 +20,19 @@ class TaskViewModel { } #createTaskInputElement(subjectId) { - const formId = `${subjectId}-add-task-form`; - const formElement = document.createElement('form'); - formElement.id = formId; - formElement.classList.add('add-task-form'); - - const inputElement = document.createElement('input'); - inputElement.type = 'text'; - inputElement.placeholder = '할 일을 입력해주세요'; - - const submitButtonElement = document.createElement('button'); - submitButtonElement.innerText = '추가'; + const formElement = createElement('form', { + id: `${subjectId}-add-task-form`, + class: 'add-task-form', + }); + const inputElement = createElement('input', { + type: 'text', + placeholder: '할 일을 입력해주세요', + }); + const buttonElement = createElement('button', { + innerText: '추가', + }); - formElement.appendChild(inputElement); - formElement.appendChild(submitButtonElement); + formElement.appendChild(inputElement, buttonElement); formElement.addEventListener('submit', (event) => { event.preventDefault(); From 5dc3e9f1d77c08301d4d21806702c2cdf4f91566 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 15:18:23 +0900 Subject: [PATCH 29/57] refactor: improve taskViewModel --- js/utils/index.js | 11 ++++++----- js/viewModels/subjectViewModel.js | 9 +++++---- js/viewModels/taskViewModel.js | 17 ++++++----------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/js/utils/index.js b/js/utils/index.js index df5f273..d115b68 100644 --- a/js/utils/index.js +++ b/js/utils/index.js @@ -1,11 +1,12 @@ function createElement(tag, attributes = {}) { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { - // if (key in element) { - // element[key] = value; - // } else { - element.setAttribute(key, value); - // } + if (key in element) { + element[key] = value; + } else { + element.setAttribute(key, value); + } }); + return element; } diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 36fa1c1..2aeeb97 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -45,9 +45,9 @@ class SubjectViewModel { return; } - const subjectElement = document.createElement('li'); - subjectElement.classList.add('subject'); - subjectElement.innerHTML = ` + const subjectElement = createElement('li', { + class: 'subject', + innerHTML: `

                  ${subject.getTitle()}

                  -
                  - +

                  Open


                  -
                    -
                    -

                    In Progress

                    - -
                    - +

                    In Progress


                    -
                      -
                      -

                      Done

                      - -
                      - +

                      Done


                      -
                        diff --git a/js/index.js b/js/index.js index ae9678d..6f5b588 100644 --- a/js/index.js +++ b/js/index.js @@ -17,15 +17,4 @@ document.addEventListener('DOMContentLoaded', () => { taskViewModel.addTask('task 2', doneSubject1.getId()); // -------------------------------------------------- - - STATE_LIST.forEach((state) => { - const addSubjectButtonElement = document.getElementById( - `add-subject-button-${state.toLowerCase()}` - ); - - addSubjectButtonElement.addEventListener('click', () => { - const subjectName = NEW_SUBJECT_NAME; // [todo] remove this line - subjectViewModel.addSubject(subjectName, state); - }); - }); }); diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 2aeeb97..5d50e69 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -39,6 +39,34 @@ class SubjectViewModel { ); subjectListElement.innerHTML = ''; + // [todo] refactor + /* ----- add subject button ----- */ + const addSubjectCardElement = createElement('li', { + class: 'subject add-subject-card', + }); + const formElement = createElement('form'); + const inputElement = createElement('input', { + type: 'text', + placeholder: '새로운 목표를 입력해주세요', + }); + const addSubjectButtonElement = createElement('button', { + class: 'add-subject-button', + id: `add-subject-button-${state}`, + }); + const iconElement = createElement('img', { + src: 'assets/addIcon.svg', + }); + addSubjectButtonElement.appendChild(iconElement); + formElement.append(inputElement, addSubjectButtonElement); + formElement.addEventListener('submit', (event) => { + event.preventDefault(); + this.addSubject(inputElement.value, state); + }); + + addSubjectCardElement.appendChild(formElement); + subjectListElement.appendChild(addSubjectCardElement); + + /* ----- subject list ----- */ this.#getSubjectsByState(state).forEach((subject) => { const subjectId = subject.getId(); if (subject.getState() !== state) { diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 18d826c..8cbdefb 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -22,7 +22,6 @@ class TaskViewModel { #createTaskInputElement(subjectId) { const formElement = createElement('form', { id: `${subjectId}-add-task-form`, - class: 'add-task-form', }); const inputElement = createElement('input', { type: 'text', @@ -30,6 +29,7 @@ class TaskViewModel { }); const buttonElement = createElement('button', { innerText: '추가', + class: 'add-task-button', }); formElement.append(inputElement, buttonElement); From a4ab27b8e2c14f830d9b2f1ee5baccecf0209fb1 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 18:40:39 +0900 Subject: [PATCH 31/57] feat: add button to delete task --- css/task.css | 31 ++++++++++++++++++------------- js/viewModels/taskViewModel.js | 17 +++++++++++++---- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/css/task.css b/css/task.css index 20e2ce9..50bdbb7 100644 --- a/css/task.css +++ b/css/task.css @@ -4,12 +4,30 @@ gap: 0.5rem; } +.task-list button { + padding: 0.25rem 0.5rem; + background: transparent; + + border: 1px solid var(--button-border); + border-radius: 1rem; + outline: none; + + color: var(--text-color); + white-space: nowrap; + + cursor: pointer; +} + .task { display: flex; align-items: center; gap: 0.5rem; } +.task p { + flex: 1; +} + .add-task-form { display: flex; gap: 0.5rem; @@ -28,16 +46,3 @@ color: var(--text-color); } -.add-task-form > button { - padding: 0.25rem 0.5rem; - background: transparent; - - border: 1px solid var(--button-border); - border-radius: 1rem; - outline: none; - - color: var(--text-color); - white-space: nowrap; - - cursor: pointer; -} diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 18d826c..6452d21 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -52,11 +52,20 @@ class TaskViewModel { taskListElement.appendChild(this.#createTaskInputElement(subjectId)); this.#getTasksBySubject(subjectId).forEach((task) => { - const taskElement = createElement('li', { class: 'task' }); - const checkboxElement = createElement('input', { type: 'checkbox' }); - const titleElement = createElement('p', { innerText: task.getTitle() }); + const taskElement = createElement('li', { + class: 'task', + }); + const checkboxElement = createElement('input', { + type: 'checkbox', + }); + const titleElement = createElement('p', { + innerText: task.getTitle(), + }); + const deleteButtonElement = createElement('button', { + innerText: '삭제', + }); - taskElement.append(checkboxElement, titleElement); + taskElement.append(checkboxElement, titleElement, deleteButtonElement); taskListElement.appendChild(taskElement); }); } From 00b78150f7876406a3b5d64debcebfb06636ceb0 Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 18:49:53 +0900 Subject: [PATCH 32/57] feat: add delete event handler --- js/models/task.js | 6 ++++++ js/viewModels/taskViewModel.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/js/models/task.js b/js/models/task.js index 76fbaa5..61d1e16 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -1,13 +1,19 @@ class Task { #title; #subjectId; + #id; constructor(title, subjectId) { this.#title = title; this.#subjectId = subjectId; + this.#id = Math.random().toString(36).substring(2); } getTitle() { return this.#title; } + + getId() { + return this.#id; + } } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index bb04138..ac89255 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -19,6 +19,16 @@ class TaskViewModel { return this.#taskList.get(subjectId) || []; } + deleteTask(targetId, subjectId) { + const taskList = this.#taskList.get(subjectId); + const targetIndex = taskList.findIndex( + (subject) => subject.getId() === targetId + ); + taskList.splice(targetIndex, 1); + + this.render(subjectId); + } + #createTaskInputElement(subjectId) { const formElement = createElement('form', { id: `${subjectId}-add-task-form`, @@ -67,6 +77,10 @@ class TaskViewModel { taskElement.append(checkboxElement, titleElement, deleteButtonElement); taskListElement.appendChild(taskElement); + + deleteButtonElement.addEventListener('click', () => { + this.deleteTask(task.getId(), subjectId); + }); }); } } From d0450490649ceab13b00d51536f89823c935bc4c Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 18:58:36 +0900 Subject: [PATCH 33/57] feat: add change event handler to checkbox & isCompleted prop to Task class --- js/models/task.js | 6 ++++++ js/viewModels/taskViewModel.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/js/models/task.js b/js/models/task.js index 61d1e16..5db6382 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -2,11 +2,17 @@ class Task { #title; #subjectId; #id; + #isCompleted; constructor(title, subjectId) { this.#title = title; this.#subjectId = subjectId; this.#id = Math.random().toString(36).substring(2); + this.#isCompleted = false; + } + + setCompleted(isCompleted) { + this.#isCompleted = isCompleted; } getTitle() { diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index ac89255..24cf8c3 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -81,6 +81,11 @@ class TaskViewModel { deleteButtonElement.addEventListener('click', () => { this.deleteTask(task.getId(), subjectId); }); + + checkboxElement.addEventListener('change', (event) => { + const isChecked = event.target.checked; + task.setCompleted(isChecked); + }); }); } } From b47174ac922e0f7f9c492732ae6494af0bd8867a Mon Sep 17 00:00:00 2001 From: psst54 Date: Fri, 6 Sep 2024 19:08:04 +0900 Subject: [PATCH 34/57] feat: toggle task --- css/task.css | 5 +++++ css/variables.css | 1 + js/models/task.js | 4 ++++ js/viewModels/taskViewModel.js | 5 +++++ 4 files changed, 15 insertions(+) diff --git a/css/task.css b/css/task.css index a11b8e0..59f92dd 100644 --- a/css/task.css +++ b/css/task.css @@ -27,3 +27,8 @@ .task p { flex: 1; } + +.isCompleted > p { + color: var(--cancel-text-color); + text-decoration: line-through; +} diff --git a/css/variables.css b/css/variables.css index c240e4c..0757ad3 100644 --- a/css/variables.css +++ b/css/variables.css @@ -11,6 +11,7 @@ --subject-background: #121212ee; --text-color: #ffffff; + --cancel-text-color: #505050; --border: #505050; --button-border: #ffffff; diff --git a/js/models/task.js b/js/models/task.js index 5db6382..ac1a300 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -22,4 +22,8 @@ class Task { getId() { return this.#id; } + + getIsCompleted() { + return this.#isCompleted; + } } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 24cf8c3..7c7f39f 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -67,6 +67,7 @@ class TaskViewModel { }); const checkboxElement = createElement('input', { type: 'checkbox', + checked: task.getIsCompleted(), }); const titleElement = createElement('p', { innerText: task.getTitle(), @@ -74,6 +75,9 @@ class TaskViewModel { const deleteButtonElement = createElement('button', { innerText: '삭제', }); + if (task.getIsCompleted()) { + taskElement.classList.toggle('isCompleted'); + } taskElement.append(checkboxElement, titleElement, deleteButtonElement); taskListElement.appendChild(taskElement); @@ -85,6 +89,7 @@ class TaskViewModel { checkboxElement.addEventListener('change', (event) => { const isChecked = event.target.checked; task.setCompleted(isChecked); + taskElement.classList.toggle('isCompleted'); }); }); } From 39a97240b495ef4cb1b615644cc5cdd979c92118 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:33:24 +0900 Subject: [PATCH 35/57] feat: add custom event to re-render --- js/index.js | 16 +--------- js/models/subject.js | 4 +-- js/models/task.js | 4 +-- js/viewModels/subjectViewModel.js | 37 +++++++++++++++++++--- js/viewModels/taskViewModel.js | 52 +++++++++++++++++++++++++------ 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/js/index.js b/js/index.js index 6f5b588..05bbf75 100644 --- a/js/index.js +++ b/js/index.js @@ -2,19 +2,5 @@ document.addEventListener('DOMContentLoaded', () => { const taskViewModel = new TaskViewModel(); const subjectViewModel = new SubjectViewModel(taskViewModel); - // [todo] remove below - // ------------------------------------------------- - - const openSubject1 = subjectViewModel.addSubject('Opened Subject 1', OPEN); - taskViewModel.addTask('task 1', openSubject1.getId()); - taskViewModel.addTask('task 2', openSubject1.getId()); - - const openSubject2 = subjectViewModel.addSubject('Opened Subject 2', OPEN); - taskViewModel.addTask('task 1', openSubject2.getId()); - - const doneSubject1 = subjectViewModel.addSubject('Done Subject 1', DONE); - taskViewModel.addTask('task 1', doneSubject1.getId()); - taskViewModel.addTask('task 2', doneSubject1.getId()); - - // -------------------------------------------------- + subjectViewModel.render(); }); diff --git a/js/models/subject.js b/js/models/subject.js index 142c1ab..00a8e65 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -11,8 +11,8 @@ class Subject { this.#id = Math.random().toString(36).substring(2); // [todo] make random id function } - addTask(task) { - this.#taskList.push(task); + setState(state) { + this.#state = state; } getTitle() { diff --git a/js/models/task.js b/js/models/task.js index ac1a300..ad21b0e 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -4,11 +4,11 @@ class Task { #id; #isCompleted; - constructor(title, subjectId) { + constructor(title, subjectId, isCompleted = false) { this.#title = title; this.#subjectId = subjectId; this.#id = Math.random().toString(36).substring(2); - this.#isCompleted = false; + this.#isCompleted = isCompleted; } setCompleted(isCompleted) { diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 5d50e69..a22485a 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -4,16 +4,13 @@ class SubjectViewModel { constructor(taskViewModel) { this.#subjectList = new Map(); + STATE_LIST.forEach((state) => this.#subjectList.set(state, [])); this.#taskViewModel = taskViewModel; } addSubject(title, state = OPEN) { const subject = new Subject(title, state); - if (!this.#subjectList.has(state)) { - this.#subjectList.set(state, []); - } this.#subjectList.get(state).push(subject); - this.render(); return subject; } @@ -48,6 +45,7 @@ class SubjectViewModel { const inputElement = createElement('input', { type: 'text', placeholder: '새로운 목표를 입력해주세요', + name: 'subject-title', }); const addSubjectButtonElement = createElement('button', { class: 'add-subject-button', @@ -61,6 +59,11 @@ class SubjectViewModel { formElement.addEventListener('submit', (event) => { event.preventDefault(); this.addSubject(inputElement.value, state); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + formElement.dispatchEvent(taskChangeEvent); }); addSubjectCardElement.appendChild(formElement); @@ -89,6 +92,27 @@ class SubjectViewModel { }); subjectListElement.appendChild(subjectElement); + subjectElement.addEventListener('taskChange', () => { + const newState = this.#taskViewModel.getSubjectState(subjectId); + const subjectList = this.#subjectList.get(state); + const targetSubject = subjectList.find( + (subject) => subject.getId() === subjectId + ); + targetSubject.setState(newState); + + const index = subjectList.indexOf(targetSubject); + + if (state === newState) { + this.#taskViewModel.render(subjectId); + return; + } + + subjectList.splice(index, 1); + this.#subjectList.get(newState).push(targetSubject); + + this.render(); + }); + const deleteButtonElement = document.getElementById( `${subjectId}-delete-button` ); @@ -96,7 +120,10 @@ class SubjectViewModel { this.deleteSubject(subjectId, state) ); - this.#taskViewModel.render(subjectId); + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + deleteButtonElement.dispatchEvent(taskChangeEvent); }); }); } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 7c7f39f..55c0428 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -5,18 +5,12 @@ class TaskViewModel { this.#taskList = new Map(); } - addTask(title, subjectId) { - const task = new Task(title, subjectId); + addTask(title, subjectId, isCompleted = false) { + const task = new Task(title, subjectId, isCompleted); if (!this.#taskList.has(subjectId)) { this.#taskList.set(subjectId, []); } this.#taskList.get(subjectId).push(task); - - this.render(subjectId); - } - - #getTasksBySubject(subjectId) { - return this.#taskList.get(subjectId) || []; } deleteTask(targetId, subjectId) { @@ -25,8 +19,10 @@ class TaskViewModel { (subject) => subject.getId() === targetId ); taskList.splice(targetIndex, 1); + } - this.render(subjectId); + #getTasksBySubject(subjectId) { + return this.#taskList.get(subjectId) || []; } #createTaskInputElement(subjectId) { @@ -36,6 +32,7 @@ class TaskViewModel { const inputElement = createElement('input', { type: 'text', placeholder: '할 일을 입력해주세요', + name: 'task-title', }); const buttonElement = createElement('button', { innerText: '추가', @@ -49,12 +46,38 @@ class TaskViewModel { if (!inputElement.value) { return; } + this.addTask(inputElement.value, subjectId); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + formElement.dispatchEvent(taskChangeEvent); }); return formElement; } + getSubjectState(subjectId) { + const taskList = this.#taskList.get(subjectId); + if (!taskList) { + return OPEN; + } + + const taskCount = taskList.length; + const doneTaskCount = taskList.filter((task) => + task.getIsCompleted() + ).length; + + if (taskCount === doneTaskCount) { + return DONE; + } + if (doneTaskCount === 0) { + return OPEN; + } + return IN_PROGRESS; + } + render(subjectId) { const taskListElement = document.getElementById(`${subjectId}-task-list`); taskListElement.innerHTML = ''; @@ -68,6 +91,7 @@ class TaskViewModel { const checkboxElement = createElement('input', { type: 'checkbox', checked: task.getIsCompleted(), + name: 'checkbox', }); const titleElement = createElement('p', { innerText: task.getTitle(), @@ -84,12 +108,22 @@ class TaskViewModel { deleteButtonElement.addEventListener('click', () => { this.deleteTask(task.getId(), subjectId); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + deleteButtonElement.dispatchEvent(taskChangeEvent); }); checkboxElement.addEventListener('change', (event) => { const isChecked = event.target.checked; task.setCompleted(isChecked); taskElement.classList.toggle('isCompleted'); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + checkboxElement.dispatchEvent(taskChangeEvent); }); }); } From b89fb7a15116c15bf9441cd1f1ba69b171bd5553 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:37:11 +0900 Subject: [PATCH 36/57] refactor: edit constructor props --- js/models/subject.js | 2 +- js/models/task.js | 2 +- js/viewModels/subjectViewModel.js | 4 ++-- js/viewModels/taskViewModel.js | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/js/models/subject.js b/js/models/subject.js index 00a8e65..052d9bd 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -4,7 +4,7 @@ class Subject { #state; #id; - constructor(title = NEW_SUBJECT_NAME, state = OPEN) { + constructor({ title = NEW_SUBJECT_NAME, state = OPEN }) { this.#title = title; this.#taskList = []; // [question] is this necessary? this.#state = state; diff --git a/js/models/task.js b/js/models/task.js index ad21b0e..e7299aa 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -4,7 +4,7 @@ class Task { #id; #isCompleted; - constructor(title, subjectId, isCompleted = false) { + constructor({ title = 'New Task', subjectId, isCompleted = false }) { this.#title = title; this.#subjectId = subjectId; this.#id = Math.random().toString(36).substring(2); diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index a22485a..26927e6 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -8,8 +8,8 @@ class SubjectViewModel { this.#taskViewModel = taskViewModel; } - addSubject(title, state = OPEN) { - const subject = new Subject(title, state); + addSubject({ title, state = OPEN }) { + const subject = new Subject({ title, state }); this.#subjectList.get(state).push(subject); this.render(); return subject; diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 55c0428..1ffc6b4 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -5,8 +5,8 @@ class TaskViewModel { this.#taskList = new Map(); } - addTask(title, subjectId, isCompleted = false) { - const task = new Task(title, subjectId, isCompleted); + addTask({ title, subjectId, isCompleted = false }) { + const task = new Task({ title, subjectId, isCompleted }); if (!this.#taskList.has(subjectId)) { this.#taskList.set(subjectId, []); } @@ -47,7 +47,7 @@ class TaskViewModel { return; } - this.addTask(inputElement.value, subjectId); + this.addTask({ title: inputElement.value, subjectId }); const taskChangeEvent = new CustomEvent('taskChange', { bubbles: true, @@ -69,12 +69,12 @@ class TaskViewModel { task.getIsCompleted() ).length; - if (taskCount === doneTaskCount) { - return DONE; - } if (doneTaskCount === 0) { return OPEN; } + if (taskCount === doneTaskCount) { + return DONE; + } return IN_PROGRESS; } From 4a25022dd255b7e9ab0596900f9ebd9082eba4f4 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:38:22 +0900 Subject: [PATCH 37/57] refactor: remove useless prop --- js/models/task.js | 4 +--- js/viewModels/taskViewModel.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/models/task.js b/js/models/task.js index e7299aa..b6ab0de 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -1,12 +1,10 @@ class Task { #title; - #subjectId; #id; #isCompleted; - constructor({ title = 'New Task', subjectId, isCompleted = false }) { + constructor({ title = 'New Task', isCompleted = false }) { this.#title = title; - this.#subjectId = subjectId; this.#id = Math.random().toString(36).substring(2); this.#isCompleted = isCompleted; } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 1ffc6b4..8a67e94 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -6,7 +6,7 @@ class TaskViewModel { } addTask({ title, subjectId, isCompleted = false }) { - const task = new Task({ title, subjectId, isCompleted }); + const task = new Task({ title, isCompleted }); if (!this.#taskList.has(subjectId)) { this.#taskList.set(subjectId, []); } From b332485bdfc8368ccd3a482f4237703da95d9d98 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:41:21 +0900 Subject: [PATCH 38/57] refactor: add string constant --- js/constant.js | 4 ++++ js/models/task.js | 2 +- js/viewModels/subjectViewModel.js | 2 +- js/viewModels/taskViewModel.js | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/js/constant.js b/js/constant.js index 3d4408f..b9698eb 100644 --- a/js/constant.js +++ b/js/constant.js @@ -4,3 +4,7 @@ const DONE = 'done'; const STATE_LIST = [OPEN, IN_PROGRESS, DONE]; const NEW_SUBJECT_NAME = 'New Subject'; +const NEW_TASK_NAME = 'New Task'; + +const NEW_SUBJECT_PLACEHOLDER = '새로운 목표를 입력해주세요'; +const NEW_TASK_PLACEHOLDER = '할 일을 입력해주세요'; diff --git a/js/models/task.js b/js/models/task.js index b6ab0de..6d24578 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -3,7 +3,7 @@ class Task { #id; #isCompleted; - constructor({ title = 'New Task', isCompleted = false }) { + constructor({ title = NEW_TASK_NAME, isCompleted = false }) { this.#title = title; this.#id = Math.random().toString(36).substring(2); this.#isCompleted = isCompleted; diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 26927e6..1379b88 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -44,7 +44,7 @@ class SubjectViewModel { const formElement = createElement('form'); const inputElement = createElement('input', { type: 'text', - placeholder: '새로운 목표를 입력해주세요', + placeholder: NEW_SUBJECT_PLACEHOLDER, name: 'subject-title', }); const addSubjectButtonElement = createElement('button', { diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 8a67e94..2ee9603 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -31,7 +31,7 @@ class TaskViewModel { }); const inputElement = createElement('input', { type: 'text', - placeholder: '할 일을 입력해주세요', + placeholder: NEW_TASK_PLACEHOLDER, name: 'task-title', }); const buttonElement = createElement('button', { From 9fcc04ad58d2f19db2e2649ca2d92162f5b67183 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:42:17 +0900 Subject: [PATCH 39/57] refactor: remove useless prop --- js/models/subject.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/js/models/subject.js b/js/models/subject.js index 052d9bd..49c1996 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -1,12 +1,10 @@ class Subject { #title; - #taskList; #state; #id; constructor({ title = NEW_SUBJECT_NAME, state = OPEN }) { this.#title = title; - this.#taskList = []; // [question] is this necessary? this.#state = state; this.#id = Math.random().toString(36).substring(2); // [todo] make random id function } @@ -23,10 +21,6 @@ class Subject { return this.#state; } - getTaskList() { - return this.#taskList; - } - getId() { return this.#id; } From 7eae8e690f285e2af02ca7adbe2adfe5ebddf5ed Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:50:39 +0900 Subject: [PATCH 40/57] refactor: add getRandomId function --- js/models/subject.js | 2 +- js/models/task.js | 2 +- js/utils/index.js | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/js/models/subject.js b/js/models/subject.js index 49c1996..78ad436 100644 --- a/js/models/subject.js +++ b/js/models/subject.js @@ -6,7 +6,7 @@ class Subject { constructor({ title = NEW_SUBJECT_NAME, state = OPEN }) { this.#title = title; this.#state = state; - this.#id = Math.random().toString(36).substring(2); // [todo] make random id function + this.#id = getRandomId(); } setState(state) { diff --git a/js/models/task.js b/js/models/task.js index 6d24578..c3aef83 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -5,7 +5,7 @@ class Task { constructor({ title = NEW_TASK_NAME, isCompleted = false }) { this.#title = title; - this.#id = Math.random().toString(36).substring(2); + this.#id = getRandomId(); this.#isCompleted = isCompleted; } diff --git a/js/utils/index.js b/js/utils/index.js index d115b68..f6ce082 100644 --- a/js/utils/index.js +++ b/js/utils/index.js @@ -10,3 +10,9 @@ function createElement(tag, attributes = {}) { return element; } + +function getRandomId() { + return Math.random() // generate random number between 0 to 1 + .toString(36) // convert to base-36 string + .substring(2); // remove leading '0.' +} From 35dc943998e0e2e71a592b24e0b29c730f745271 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 02:58:52 +0900 Subject: [PATCH 41/57] fix: fix function call with wrong props --- js/viewModels/subjectViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 1379b88..f89af75 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -58,7 +58,7 @@ class SubjectViewModel { formElement.append(inputElement, addSubjectButtonElement); formElement.addEventListener('submit', (event) => { event.preventDefault(); - this.addSubject(inputElement.value, state); + this.addSubject({ title: inputElement.value, state }); const taskChangeEvent = new CustomEvent('taskChange', { bubbles: true, From 842f67534842a592c424303445c42d7b2f7b4bc8 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 03:01:30 +0900 Subject: [PATCH 42/57] docs: add comment for constant --- js/constant.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/constant.js b/js/constant.js index b9698eb..f0c8b2d 100644 --- a/js/constant.js +++ b/js/constant.js @@ -1,10 +1,13 @@ +// kanban board columns name const OPEN = 'open'; const IN_PROGRESS = 'in-progress'; const DONE = 'done'; const STATE_LIST = [OPEN, IN_PROGRESS, DONE]; +// default name for constructor const NEW_SUBJECT_NAME = 'New Subject'; const NEW_TASK_NAME = 'New Task'; +// placeholder for input field const NEW_SUBJECT_PLACEHOLDER = '새로운 목표를 입력해주세요'; const NEW_TASK_PLACEHOLDER = '할 일을 입력해주세요'; From eb9ef18d67908e27c0e091e9b79f6d3b408d4bdc Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 03:01:50 +0900 Subject: [PATCH 43/57] chore: rename css file --- css/{layout.css => kanban.css} | 0 index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename css/{layout.css => kanban.css} (100%) diff --git a/css/layout.css b/css/kanban.css similarity index 100% rename from css/layout.css rename to css/kanban.css diff --git a/index.html b/index.html index 8bfe49d..3870fb6 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Vanilla Todo - + From 1e0c467bbf4bf53886a115c1651e3c6a258dfaad Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 03:03:13 +0900 Subject: [PATCH 44/57] fix: edit css file --- css/subject.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/subject.css b/css/subject.css index 1e53d75..56b951d 100644 --- a/css/subject.css +++ b/css/subject.css @@ -19,7 +19,7 @@ } .add-subject-card { - display: 'flex'; + display: flex; } .add-subject-button { From 1ba6be6a16e3ff13fb8a59bd540c69d3935e3e72 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 03:29:57 +0900 Subject: [PATCH 45/57] refactor: split functions in subjectViewModel --- js/viewModels/subjectViewModel.js | 207 +++++++++++++++++------------- 1 file changed, 119 insertions(+), 88 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index f89af75..7b2edc8 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -4,22 +4,30 @@ class SubjectViewModel { constructor(taskViewModel) { this.#subjectList = new Map(); - STATE_LIST.forEach((state) => this.#subjectList.set(state, [])); + STATE_LIST.forEach((state) => this.#subjectList.set(state, [])); // init all columns this.#taskViewModel = taskViewModel; } addSubject({ title, state = OPEN }) { - const subject = new Subject({ title, state }); - this.#subjectList.get(state).push(subject); + const newSubject = new Subject({ title, state }); + + this.#subjectList + .get(state) // get the column where the subject will be inserted + .push(newSubject); + this.render(); - return subject; + return newSubject; } deleteSubject(targetId, state) { + // get the column from which the subject will be removed const subjectList = this.#subjectList.get(state); + + // get the index of the target subject by its ID const targetIndex = subjectList.findIndex( (subject) => subject.getId() === targetId ); + // remove the target subject from the column subjectList.splice(targetIndex, 1); this.render(); @@ -29,101 +37,124 @@ class SubjectViewModel { return this.#subjectList.get(state) || []; } + #createAddSubjectForm(state) { + const addSubjectCardElement = createElement('li', { + class: 'subject add-subject-card', + }); + const formElement = createElement('form'); + const inputElement = createElement('input', { + type: 'text', + placeholder: NEW_SUBJECT_PLACEHOLDER, + name: 'subject-title', + }); + const addSubjectButtonElement = createElement('button', { + class: 'add-subject-button', + id: `add-subject-button-${state}`, + }); + const iconElement = createElement('img', { + src: 'assets/addIcon.svg', + }); + addSubjectButtonElement.appendChild(iconElement); + formElement.append(inputElement, addSubjectButtonElement); + + formElement.addEventListener('submit', (event) => + this.#handleAddSubjectSubmit(event, inputElement, state, formElement) + ); + + addSubjectCardElement.appendChild(formElement); + return addSubjectCardElement; + } + + #handleAddSubjectSubmit(event, inputElement, state, formElement) { + event.preventDefault(); + this.addSubject({ title: inputElement.value, state }); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + formElement.dispatchEvent(taskChangeEvent); + } + + #createSubjectElement(subject) { + const subjectId = subject.getId(); + const subjectElement = createElement('li', { + class: 'subject', + innerHTML: ` +
                        +

                        ${subject.getTitle()}

                        + +
                        +
                        +
                          +
                        + `, + }); + + return subjectElement; + } + + #attachEventHandlerToSubject(subjectElement, subjectId, state) { + subjectElement.addEventListener('taskChange', () => + this.#handleTaskChange(subjectId, state) + ); + + const deleteButtonElement = document.getElementById( + `${subjectId}-delete-button` + ); + deleteButtonElement.addEventListener('click', () => + this.deleteSubject(subjectId, state) + ); + + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + deleteButtonElement.dispatchEvent(taskChangeEvent); + } + + #handleTaskChange(subjectId, state) { + const newState = this.#taskViewModel.getSubjectState(subjectId); + const subjectList = this.#subjectList.get(state); + const targetSubject = subjectList.find( + (subject) => subject.getId() === subjectId + ); + targetSubject.setState(newState); + + if (state === newState) { + // 1. only render task list of subject + this.#taskViewModel.render(subjectId); + return; + } + + subjectList.splice(subjectList.indexOf(targetSubject), 1); + this.#subjectList.get(newState).push(targetSubject); + + // 2. render all the column + this.render(); + } + render() { STATE_LIST.forEach((state) => { const subjectListElement = document.getElementById( `${state}-subject-list` ); - subjectListElement.innerHTML = ''; - // [todo] refactor - /* ----- add subject button ----- */ - const addSubjectCardElement = createElement('li', { - class: 'subject add-subject-card', - }); - const formElement = createElement('form'); - const inputElement = createElement('input', { - type: 'text', - placeholder: NEW_SUBJECT_PLACEHOLDER, - name: 'subject-title', - }); - const addSubjectButtonElement = createElement('button', { - class: 'add-subject-button', - id: `add-subject-button-${state}`, - }); - const iconElement = createElement('img', { - src: 'assets/addIcon.svg', - }); - addSubjectButtonElement.appendChild(iconElement); - formElement.append(inputElement, addSubjectButtonElement); - formElement.addEventListener('submit', (event) => { - event.preventDefault(); - this.addSubject({ title: inputElement.value, state }); - - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - formElement.dispatchEvent(taskChangeEvent); - }); + // init column + subjectListElement.innerHTML = ''; - addSubjectCardElement.appendChild(formElement); - subjectListElement.appendChild(addSubjectCardElement); + // render form to add subject + subjectListElement.appendChild(this.#createAddSubjectForm(state)); - /* ----- subject list ----- */ + // render subject this.#getSubjectsByState(state).forEach((subject) => { - const subjectId = subject.getId(); - if (subject.getState() !== state) { - return; - } - - const subjectElement = createElement('li', { - class: 'subject', - innerHTML: ` -
                        -

                        ${subject.getTitle()}

                        - -
                        -
                        -
                          -
                        - `, - }); + const subjectElement = this.#createSubjectElement(subject); subjectListElement.appendChild(subjectElement); - - subjectElement.addEventListener('taskChange', () => { - const newState = this.#taskViewModel.getSubjectState(subjectId); - const subjectList = this.#subjectList.get(state); - const targetSubject = subjectList.find( - (subject) => subject.getId() === subjectId - ); - targetSubject.setState(newState); - - const index = subjectList.indexOf(targetSubject); - - if (state === newState) { - this.#taskViewModel.render(subjectId); - return; - } - - subjectList.splice(index, 1); - this.#subjectList.get(newState).push(targetSubject); - - this.render(); - }); - - const deleteButtonElement = document.getElementById( - `${subjectId}-delete-button` - ); - deleteButtonElement.addEventListener('click', () => - this.deleteSubject(subjectId, state) + this.#attachEventHandlerToSubject( + subjectElement, + subject.getId(), + state ); - - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - deleteButtonElement.dispatchEvent(taskChangeEvent); }); }); } From 7fdc9f715c3660991367b14b8b841f1f8416b2f8 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 03:47:55 +0900 Subject: [PATCH 46/57] docs: add comment & jsdoc --- js/viewModels/subjectViewModel.js | 73 +++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 7b2edc8..4cb7ac4 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -4,39 +4,65 @@ class SubjectViewModel { constructor(taskViewModel) { this.#subjectList = new Map(); - STATE_LIST.forEach((state) => this.#subjectList.set(state, [])); // init all columns + STATE_LIST.forEach((state) => this.#subjectList.set(state, [])); // Init all columns this.#taskViewModel = taskViewModel; } + /** + * Adds a new subject to the specified state column and updates the view. + * + * @param {string} params.title - The title of the new subject. + * @param {string} [params.state=OPEN] - The state column where the subject will be added. Default value is `OPEN`. + * + * @returns {Subject} The newly created subject. + */ addSubject({ title, state = OPEN }) { const newSubject = new Subject({ title, state }); this.#subjectList - .get(state) // get the column where the subject will be inserted + .get(state) // Get the column where the subject will be inserted .push(newSubject); this.render(); return newSubject; } - deleteSubject(targetId, state) { - // get the column from which the subject will be removed + /** + * Removes a subject from the specified state column and updates the view. + * + * @param {string} params.targetId - The ID of the subject to be removed. + * @param {string} params.state - The state column from which the subject will be removed. + */ + deleteSubject({ targetId, state }) { + // Get the column from which the subject will be removed const subjectList = this.#subjectList.get(state); - // get the index of the target subject by its ID + // Get the index of the target subject by its ID const targetIndex = subjectList.findIndex( (subject) => subject.getId() === targetId ); - // remove the target subject from the column + // Remove the target subject from the column subjectList.splice(targetIndex, 1); this.render(); } + /** + * Retrieves the list of subjects for a given state. + * + * @param {string} state - The state for which to retrieve subjects. + * @returns {Subject[]} - An array of subjects associated with the specified state. + */ #getSubjectsByState(state) { return this.#subjectList.get(state) || []; } + /** + * Creates and returns a form element for adding a new subject. + * + * @param {string} state - The state in which the new subject will be added. + * @returns {HTMLElement} - The created form element. + */ #createAddSubjectForm(state) { const addSubjectCardElement = createElement('li', { class: 'subject add-subject-card', @@ -75,6 +101,12 @@ class SubjectViewModel { formElement.dispatchEvent(taskChangeEvent); } + /** + * Creates a card element for a subject. + * + * @param {Subject} subject - The subject object used to create the DOM element. + * @returns {HTMLLIElement} The created element of a subject. + */ #createSubjectElement(subject) { const subjectId = subject.getId(); const subjectElement = createElement('li', { @@ -104,7 +136,7 @@ class SubjectViewModel { `${subjectId}-delete-button` ); deleteButtonElement.addEventListener('click', () => - this.deleteSubject(subjectId, state) + this.deleteSubject({ targetId: subjectId, state }) ); const taskChangeEvent = new CustomEvent('taskChange', { @@ -115,22 +147,27 @@ class SubjectViewModel { #handleTaskChange(subjectId, state) { const newState = this.#taskViewModel.getSubjectState(subjectId); - const subjectList = this.#subjectList.get(state); - const targetSubject = subjectList.find( - (subject) => subject.getId() === subjectId + const currentColumnSubjectList = this.#subjectList.get(state); + const subjectToUpdate = currentColumnSubjectList.find( + (currentSubject) => currentSubject.getId() === subjectId ); - targetSubject.setState(newState); + subjectToUpdate.setState(newState); if (state === newState) { - // 1. only render task list of subject + // Case #1 + // If the state is not changed, only update the task list for the subject this.#taskViewModel.render(subjectId); return; } - subjectList.splice(subjectList.indexOf(targetSubject), 1); - this.#subjectList.get(newState).push(targetSubject); + currentColumnSubjectList.splice( + currentColumnSubjectList.indexOf(subjectToUpdate), + 1 + ); + this.#subjectList.get(newState).push(subjectToUpdate); - // 2. render all the column + // Case #2 + // Re-render the entire column this.render(); } @@ -140,13 +177,13 @@ class SubjectViewModel { `${state}-subject-list` ); - // init column + // Init column subjectListElement.innerHTML = ''; - // render form to add subject + // Render form to add subject subjectListElement.appendChild(this.#createAddSubjectForm(state)); - // render subject + // Render subject this.#getSubjectsByState(state).forEach((subject) => { const subjectElement = this.#createSubjectElement(subject); subjectListElement.appendChild(subjectElement); From 9c701615c8be37a4ac31ed7ad70a3f468eed8922 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 04:36:09 +0900 Subject: [PATCH 47/57] style: add form width --- css/global.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/global.css b/css/global.css index bc548eb..b0e9a16 100644 --- a/css/global.css +++ b/css/global.css @@ -9,6 +9,7 @@ body { form { display: flex; gap: 0.5rem; + width: 100%; } input[type='text'] { From ee16790ca0eeb6f4f34ddb3f9c29511a35d3e702 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 04:40:44 +0900 Subject: [PATCH 48/57] feat: show title date --- index.html | 2 +- js/index.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 3870fb6..55ea9bb 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@
                        -

                        20XX. XX. XX.

                        +

                        diff --git a/js/index.js b/js/index.js index 05bbf75..1203229 100644 --- a/js/index.js +++ b/js/index.js @@ -1,4 +1,13 @@ document.addEventListener('DOMContentLoaded', () => { + // Set title date. + const titleElement = document.getElementById('title'); + titleElement.innerText = new Intl.DateTimeFormat('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).format(new Date()); + + // Init kanban board const taskViewModel = new TaskViewModel(); const subjectViewModel = new SubjectViewModel(taskViewModel); From 5ae4943a20bc3795547faf2caacd4b79d8efba37 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 04:43:13 +0900 Subject: [PATCH 49/57] chore: rename setIsCompleted method --- js/models/task.js | 2 +- js/viewModels/taskViewModel.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/models/task.js b/js/models/task.js index c3aef83..9a851c3 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -9,7 +9,7 @@ class Task { this.#isCompleted = isCompleted; } - setCompleted(isCompleted) { + setIsCompleted(isCompleted) { this.#isCompleted = isCompleted; } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 2ee9603..c2adc11 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -117,7 +117,7 @@ class TaskViewModel { checkboxElement.addEventListener('change', (event) => { const isChecked = event.target.checked; - task.setCompleted(isChecked); + task.setIsCompleted(isChecked); taskElement.classList.toggle('isCompleted'); const taskChangeEvent = new CustomEvent('taskChange', { From 4983c456563998d40677871fdfa565d46a1fe170 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 04:49:03 +0900 Subject: [PATCH 50/57] docs: add comment for createElement function --- js/utils/index.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/js/utils/index.js b/js/utils/index.js index f6ce082..adc64fa 100644 --- a/js/utils/index.js +++ b/js/utils/index.js @@ -1,9 +1,25 @@ +/** + * Creates a new HTML element with specified attributes. + * + * @param {string} tag - The type of element. + * @param {Object} [attributes={}] - An object of the attributes to set on the element. + * + * @returns {HTMLElement} - The created element with the specified attributes. + */ function createElement(tag, attributes = {}) { const element = document.createElement(tag); + + // Iterate provided attributes Object.entries(attributes).forEach(([key, value]) => { if (key in element) { + // Case #1 + // If the attribute key is a property of the element, set it. + // i.e., properties like `value`, `checked`, `textContent` element[key] = value; } else { + // Case #2 + // If it is not, set it as an HTML attribute. + // i.e., attributes like `type`, `id`, `class` element.setAttribute(key, value); } }); @@ -12,7 +28,7 @@ function createElement(tag, attributes = {}) { } function getRandomId() { - return Math.random() // generate random number between 0 to 1 - .toString(36) // convert to base-36 string - .substring(2); // remove leading '0.' + return Math.random() // Generate random number between 0 to 1. + .toString(36) // Convert to base-36 string. + .substring(2); // Remove leading '0.'. } From 40e8b363818fb84ce2dab75608fe0e84cd627564 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 05:22:06 +0900 Subject: [PATCH 51/57] style: change styles --- css/global.css | 7 ++++++ css/kanban.css | 40 +++++++++++++++++------------ css/subject.css | 29 +++++---------------- index.html | 42 ++++++++++++++++--------------- js/viewModels/subjectViewModel.js | 5 ++-- 5 files changed, 61 insertions(+), 62 deletions(-) diff --git a/css/global.css b/css/global.css index b0e9a16..ee0a9e3 100644 --- a/css/global.css +++ b/css/global.css @@ -6,6 +6,13 @@ body { background-color: var(--background); } +h1 { + font-size: 4rem; + + color: transparent; + -webkit-text-stroke: 1px var(--text-color); +} + form { display: flex; gap: 0.5rem; diff --git a/css/kanban.css b/css/kanban.css index 3805d5b..6ad8209 100644 --- a/css/kanban.css +++ b/css/kanban.css @@ -1,42 +1,50 @@ -.container { - display: 'flex'; - padding: 2rem; -} - -.header { - margin-bottom: 1rem; -} -header > h1 { - font-size: 3rem; +.wrapper { + display: flex; + justify-content: center; } -.title { - text-align: center; +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; - color: transparent; - -webkit-text-stroke: 1px var(--text-color); + padding: 2rem; + width: 100%; + max-width: 1200px; } .kanban-board { display: flex; - gap: 0.5rem; + gap: 1rem; width: 100%; } -.board-column { +.kanban-board section { display: flex; flex-direction: column; gap: 1rem; flex: 1; + height: fit-content; padding: 1rem; border: 3px solid; border-radius: 1rem; } +@media (max-width: 1000px) { + .container { + max-width: 600px; + } + + .kanban-board { + flex-direction: column; + } +} + #open-column { background-color: var(--open-column-background); border-color: var(--open-column-border); diff --git a/css/subject.css b/css/subject.css index 56b951d..b188934 100644 --- a/css/subject.css +++ b/css/subject.css @@ -13,38 +13,21 @@ border-radius: 1rem; } -.subject-header { +.subject header { display: flex; justify-content: space-between; } -.add-subject-card { - display: flex; -} - -.add-subject-button { +.add-subject-card button, +.delete-subject-button { background: transparent; - border: none; - border-radius: 100%; outline: none; - cursor: pointer; } -.add-subject-button > img { + +.add-subject-card button img, +.delete-subject-button img { width: 2rem; height: 2rem; } - -.delete-subject-button { - background: transparent; - - border: none; - outline: none; - - cursor: pointer; -} -.delete-subject-icon { - width: 1.5rem; - height: 1.5rem; -} diff --git a/index.html b/index.html index 55ea9bb..af62680 100644 --- a/index.html +++ b/index.html @@ -13,29 +13,31 @@ -
                        -
                        -

                        -
                        +
                        +
                        +
                        +

                        +
                        -
                        -
                        -

                        Open

                        -
                        -
                          -
                          +
                          +
                          +

                          Open

                          +
                          +
                            +
                            -
                            -

                            In Progress

                            -
                            -
                              -
                              +
                              +

                              In Progress

                              +
                              +
                                +
                                -
                                -

                                Done

                                -
                                -
                                  -
                                  +
                                  +

                                  Done

                                  +
                                  +
                                    +
                                    +
                                    diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 4cb7ac4..ba76b2b 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -74,7 +74,6 @@ class SubjectViewModel { name: 'subject-title', }); const addSubjectButtonElement = createElement('button', { - class: 'add-subject-button', id: `add-subject-button-${state}`, }); const iconElement = createElement('img', { @@ -112,10 +111,10 @@ class SubjectViewModel { const subjectElement = createElement('li', { class: 'subject', innerHTML: ` -
                                    +

                                    ${subject.getTitle()}

                                    From 37e1af801856422b5f5211972ce0cb4d1a0adae5 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 05:43:36 +0900 Subject: [PATCH 52/57] refactor: dispatchTaskChangeEvent function --- js/utils/index.js | 7 +++++++ js/viewModels/subjectViewModel.js | 12 ++---------- js/viewModels/taskViewModel.js | 16 +++------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/js/utils/index.js b/js/utils/index.js index adc64fa..e7ea300 100644 --- a/js/utils/index.js +++ b/js/utils/index.js @@ -32,3 +32,10 @@ function getRandomId() { .toString(36) // Convert to base-36 string. .substring(2); // Remove leading '0.'. } + +function dispatchTaskChangeEvent(element) { + const taskChangeEvent = new CustomEvent('taskChange', { + bubbles: true, + }); + element.dispatchEvent(taskChangeEvent); +} diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index ba76b2b..a44fdfc 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -93,11 +93,7 @@ class SubjectViewModel { #handleAddSubjectSubmit(event, inputElement, state, formElement) { event.preventDefault(); this.addSubject({ title: inputElement.value, state }); - - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - formElement.dispatchEvent(taskChangeEvent); + dispatchTaskChangeEvent(formElement); } /** @@ -137,11 +133,6 @@ class SubjectViewModel { deleteButtonElement.addEventListener('click', () => this.deleteSubject({ targetId: subjectId, state }) ); - - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - deleteButtonElement.dispatchEvent(taskChangeEvent); } #handleTaskChange(subjectId, state) { @@ -191,6 +182,7 @@ class SubjectViewModel { subject.getId(), state ); + dispatchTaskChangeEvent(subjectElement); }); }); } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index c2adc11..1ffeee7 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -49,10 +49,7 @@ class TaskViewModel { this.addTask({ title: inputElement.value, subjectId }); - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - formElement.dispatchEvent(taskChangeEvent); + dispatchTaskChangeEvent(formElement); }); return formElement; @@ -108,11 +105,7 @@ class TaskViewModel { deleteButtonElement.addEventListener('click', () => { this.deleteTask(task.getId(), subjectId); - - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - deleteButtonElement.dispatchEvent(taskChangeEvent); + dispatchTaskChangeEvent(deleteButtonElement); }); checkboxElement.addEventListener('change', (event) => { @@ -120,10 +113,7 @@ class TaskViewModel { task.setIsCompleted(isChecked); taskElement.classList.toggle('isCompleted'); - const taskChangeEvent = new CustomEvent('taskChange', { - bubbles: true, - }); - checkboxElement.dispatchEvent(taskChangeEvent); + dispatchTaskChangeEvent(checkboxElement); }); }); } From 75ca24ebde27c79548ee3d79455e7472bbd5354f Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 05:56:24 +0900 Subject: [PATCH 53/57] chore: rename function and variables & add comments --- js/viewModels/subjectViewModel.js | 63 +++++++++++++++++-------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index a44fdfc..73748d2 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -63,8 +63,8 @@ class SubjectViewModel { * @param {string} state - The state in which the new subject will be added. * @returns {HTMLElement} - The created form element. */ - #createAddSubjectForm(state) { - const addSubjectCardElement = createElement('li', { + #createFormElement(state) { + const containerElement = createElement('li', { class: 'subject add-subject-card', }); const formElement = createElement('form'); @@ -73,24 +73,30 @@ class SubjectViewModel { placeholder: NEW_SUBJECT_PLACEHOLDER, name: 'subject-title', }); - const addSubjectButtonElement = createElement('button', { + const buttonElement = createElement('button', { id: `add-subject-button-${state}`, }); const iconElement = createElement('img', { src: 'assets/addIcon.svg', }); - addSubjectButtonElement.appendChild(iconElement); - formElement.append(inputElement, addSubjectButtonElement); + + containerElement.appendChild(formElement); + buttonElement.appendChild(iconElement); + formElement.append(inputElement, buttonElement); formElement.addEventListener('submit', (event) => - this.#handleAddSubjectSubmit(event, inputElement, state, formElement) + this.#onSubmitAddSubject({ + event, + state, + formElement, + inputElement, + }) ); - addSubjectCardElement.appendChild(formElement); - return addSubjectCardElement; + return containerElement; } - #handleAddSubjectSubmit(event, inputElement, state, formElement) { + #onSubmitAddSubject({ event, state, formElement, inputElement }) { event.preventDefault(); this.addSubject({ title: inputElement.value, state }); dispatchTaskChangeEvent(formElement); @@ -122,28 +128,33 @@ class SubjectViewModel { return subjectElement; } - #attachEventHandlerToSubject(subjectElement, subjectId, state) { + #addSubjectEventHandler(subjectElement, subjectId, state) { + // When a taskChange event occurs, update the subject's task list. subjectElement.addEventListener('taskChange', () => - this.#handleTaskChange(subjectId, state) + this.#onChangeTask(subjectId, state) ); - const deleteButtonElement = document.getElementById( - `${subjectId}-delete-button` - ); - deleteButtonElement.addEventListener('click', () => - this.deleteSubject({ targetId: subjectId, state }) - ); + // When the delete button is clicked, remove the subject from the list. + document + .getElementById(`${subjectId}-delete-button`) + .addEventListener('click', () => + this.deleteSubject({ targetId: subjectId, state }) + ); } - #handleTaskChange(subjectId, state) { - const newState = this.#taskViewModel.getSubjectState(subjectId); + #onChangeTask(subjectId, state) { + /// Get the next state of the subject. + const nextState = this.#taskViewModel.getSubjectState(subjectId); + // Get the subject list in the current column. const currentColumnSubjectList = this.#subjectList.get(state); + // Find the subject to update based on its ID const subjectToUpdate = currentColumnSubjectList.find( (currentSubject) => currentSubject.getId() === subjectId ); - subjectToUpdate.setState(newState); + // Update the subject's state + subjectToUpdate.setState(nextState); - if (state === newState) { + if (state === nextState) { // Case #1 // If the state is not changed, only update the task list for the subject this.#taskViewModel.render(subjectId); @@ -154,7 +165,7 @@ class SubjectViewModel { currentColumnSubjectList.indexOf(subjectToUpdate), 1 ); - this.#subjectList.get(newState).push(subjectToUpdate); + this.#subjectList.get(nextState).push(subjectToUpdate); // Case #2 // Re-render the entire column @@ -171,17 +182,13 @@ class SubjectViewModel { subjectListElement.innerHTML = ''; // Render form to add subject - subjectListElement.appendChild(this.#createAddSubjectForm(state)); + subjectListElement.appendChild(this.#createFormElement(state)); // Render subject this.#getSubjectsByState(state).forEach((subject) => { const subjectElement = this.#createSubjectElement(subject); subjectListElement.appendChild(subjectElement); - this.#attachEventHandlerToSubject( - subjectElement, - subject.getId(), - state - ); + this.#addSubjectEventHandler(subjectElement, subject.getId(), state); dispatchTaskChangeEvent(subjectElement); }); }); From ebbc851cc350cb1eb6b89affc282a71af122897a Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 06:12:17 +0900 Subject: [PATCH 54/57] refactor: refactor taskViewModel --- js/models/task.js | 8 ++- js/viewModels/taskViewModel.js | 107 +++++++++++++++++++-------------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/js/models/task.js b/js/models/task.js index 9a851c3..e54b79d 100644 --- a/js/models/task.js +++ b/js/models/task.js @@ -2,11 +2,13 @@ class Task { #title; #id; #isCompleted; + #subjectId; - constructor({ title = NEW_TASK_NAME, isCompleted = false }) { + constructor({ title = NEW_TASK_NAME, isCompleted = false, subjectId }) { this.#title = title; this.#id = getRandomId(); this.#isCompleted = isCompleted; + this.#subjectId = subjectId; } setIsCompleted(isCompleted) { @@ -24,4 +26,8 @@ class Task { getIsCompleted() { return this.#isCompleted; } + + getSubjectId() { + return this.#subjectId; + } } diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index 1ffeee7..a95d2d9 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -6,18 +6,22 @@ class TaskViewModel { } addTask({ title, subjectId, isCompleted = false }) { - const task = new Task({ title, isCompleted }); if (!this.#taskList.has(subjectId)) { this.#taskList.set(subjectId, []); } - this.#taskList.get(subjectId).push(task); + this.#taskList + .get(subjectId) + .push(new Task({ title, isCompleted, subjectId })); } deleteTask(targetId, subjectId) { + // Get the task list from which the task will be removed const taskList = this.#taskList.get(subjectId); - const targetIndex = taskList.findIndex( - (subject) => subject.getId() === targetId - ); + + // Get the index of the target task by its ID + const targetIndex = taskList.findIndex((task) => task.getId() === targetId); + + // Remove the target task from the list taskList.splice(targetIndex, 1); } @@ -25,7 +29,7 @@ class TaskViewModel { return this.#taskList.get(subjectId) || []; } - #createTaskInputElement(subjectId) { + #createFormElement(subjectId) { const formElement = createElement('form', { id: `${subjectId}-add-task-form`, }); @@ -55,21 +59,64 @@ class TaskViewModel { return formElement; } + #createTaskElement(task) { + const taskElement = createElement('li', { + class: 'task', + }); + const checkboxElement = createElement('input', { + type: 'checkbox', + checked: task.getIsCompleted(), + name: 'checkbox', + }); + const titleElement = createElement('p', { + innerText: task.getTitle(), + }); + const deleteButtonElement = createElement('button', { + innerText: '삭제', + }); + if (task.getIsCompleted()) { + taskElement.classList.toggle('isCompleted'); + } + + taskElement.append(checkboxElement, titleElement, deleteButtonElement); + this.#addTaskEventHandler({ + task, + deleteButtonElement, + checkboxElement, + }); + + return taskElement; + } + + #addTaskEventHandler({ task, deleteButtonElement, checkboxElement }) { + deleteButtonElement.addEventListener('click', () => { + this.deleteTask(task.getId(), task.getSubjectId()); + dispatchTaskChangeEvent(deleteButtonElement); + }); + + checkboxElement.addEventListener('change', (event) => { + task.setIsCompleted(event.target.checked); + taskElement.classList.toggle('isCompleted'); + + dispatchTaskChangeEvent(checkboxElement); + }); + } + getSubjectState(subjectId) { - const taskList = this.#taskList.get(subjectId); - if (!taskList) { + const currentTaskList = this.#taskList.get(subjectId); + if (!currentTaskList) { return OPEN; } - const taskCount = taskList.length; - const doneTaskCount = taskList.filter((task) => + const totalTaskCount = currentTaskList.length; + const doneTaskCount = currentTaskList.filter((task) => task.getIsCompleted() ).length; if (doneTaskCount === 0) { return OPEN; } - if (taskCount === doneTaskCount) { + if (totalTaskCount === doneTaskCount) { return DONE; } return IN_PROGRESS; @@ -79,42 +126,12 @@ class TaskViewModel { const taskListElement = document.getElementById(`${subjectId}-task-list`); taskListElement.innerHTML = ''; - taskListElement.appendChild(this.#createTaskInputElement(subjectId)); + // Render form to add task + taskListElement.appendChild(this.#createFormElement(subjectId)); + // Render task this.#getTasksBySubject(subjectId).forEach((task) => { - const taskElement = createElement('li', { - class: 'task', - }); - const checkboxElement = createElement('input', { - type: 'checkbox', - checked: task.getIsCompleted(), - name: 'checkbox', - }); - const titleElement = createElement('p', { - innerText: task.getTitle(), - }); - const deleteButtonElement = createElement('button', { - innerText: '삭제', - }); - if (task.getIsCompleted()) { - taskElement.classList.toggle('isCompleted'); - } - - taskElement.append(checkboxElement, titleElement, deleteButtonElement); - taskListElement.appendChild(taskElement); - - deleteButtonElement.addEventListener('click', () => { - this.deleteTask(task.getId(), subjectId); - dispatchTaskChangeEvent(deleteButtonElement); - }); - - checkboxElement.addEventListener('change', (event) => { - const isChecked = event.target.checked; - task.setIsCompleted(isChecked); - taskElement.classList.toggle('isCompleted'); - - dispatchTaskChangeEvent(checkboxElement); - }); + taskListElement.appendChild(this.#createTaskElement(task)); }); } } From 2fe4a0d241f6f575f6e6add0279dfe62e1350d2d Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 06:19:17 +0900 Subject: [PATCH 55/57] hotfix: fix missing prop issue in function call --- js/viewModels/taskViewModel.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/js/viewModels/taskViewModel.js b/js/viewModels/taskViewModel.js index a95d2d9..db7bd2e 100644 --- a/js/viewModels/taskViewModel.js +++ b/js/viewModels/taskViewModel.js @@ -78,9 +78,15 @@ class TaskViewModel { taskElement.classList.toggle('isCompleted'); } - taskElement.append(checkboxElement, titleElement, deleteButtonElement); + taskElement.append( + checkboxElement, + + titleElement, + deleteButtonElement + ); this.#addTaskEventHandler({ task, + taskElement, deleteButtonElement, checkboxElement, }); @@ -88,7 +94,12 @@ class TaskViewModel { return taskElement; } - #addTaskEventHandler({ task, deleteButtonElement, checkboxElement }) { + #addTaskEventHandler({ + task, + taskElement, + deleteButtonElement, + checkboxElement, + }) { deleteButtonElement.addEventListener('click', () => { this.deleteTask(task.getId(), task.getSubjectId()); dispatchTaskChangeEvent(deleteButtonElement); From d9679efc7a656dcd671356d0e85e2e84b5460a75 Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 15:28:46 +0900 Subject: [PATCH 56/57] hotfix: remove subject form in in-progress, done column --- js/viewModels/subjectViewModel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/viewModels/subjectViewModel.js b/js/viewModels/subjectViewModel.js index 73748d2..2090f20 100644 --- a/js/viewModels/subjectViewModel.js +++ b/js/viewModels/subjectViewModel.js @@ -182,7 +182,9 @@ class SubjectViewModel { subjectListElement.innerHTML = ''; // Render form to add subject - subjectListElement.appendChild(this.#createFormElement(state)); + if (state === OPEN) { + subjectListElement.appendChild(this.#createFormElement(state)); + } // Render subject this.#getSubjectsByState(state).forEach((subject) => { From 2727fb9ecffa2fe4dcd07baf238eabfe80281acf Mon Sep 17 00:00:00 2001 From: psst54 Date: Sat, 7 Sep 2024 16:04:15 +0900 Subject: [PATCH 57/57] docs: write readme --- README.md | 80 +++++++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index c258cda..3ab89f7 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,54 @@ # 1주차 미션: Vanilla-Todo -# 서론 +# 결과물 -안녕하세요 🙌🏻 20기 프론트엔드 운영진 **이지인**입니다. +배포 링크 : +https://vanilla-todo-20th-lovat.vercel.app/ -이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다. +## 기능 구현 -비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 **창의성**을 충분히 발휘해 보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 고민해 보시고, 본인이 생각한 가장 창의적인 방법으로 코드를 작성해 주세요. 여러분이 미션을 수행하는 과정에서 겪는 고민과 생각의 깊이만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다. +- Open, In Progress, Done column별로 목표를 확인할 수 있다. +- 각 Column에서 input field를 통해 새로운 목표를 추가할 수 있다. +- 목표는 X 버튼을 통해서 삭제할 수 있다. +- 목표 내에서 input field를 통해 새로운 할 일을 추가할 수 있다. +- 할 일은 삭제 버튼을 통해서 삭제할 수 있다. +- 할 일 요소의 체크박스틀 통해 할 일을 완료/해제할 수 있다. -막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 프론트엔드 카톡방에 편하게 질문을 남겨 주세요! +# Key Questions -# 미션 +## DOM은 무엇인가요? -## 미션 목표 +DOM(Document Object Model)은 HTML 또는 XML 문서의 구조를 표현하는 인터페이스로, 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공합니다. -- VSCode, Prettier를 이용하여 개발 환경을 관리합니다. -- HTML/CSS의 기초를 이해합니다. -- JavaScript를 이용한 DOM 조작을 이해합니다. -- Vanilla Js를 이용한 어플리케이션 상태 관리 방법을 이해합니다. +예를 들어서, JS라는 프로그래밍 언어로 HTML 문서의 구조, 스타일, 내용을 변경할 수 있습니다. -## 기한 +  -- 2024년 9월 7일 토요일 +HTML 문서가 있다면, 브라우저는 HTML 문서를 읽어들이고 HTML의 각 요소(element)를 Node라는 객체로 표현합니다. `

                                    `~`

                                    ` 태그는 `HTMLHeadingElement` 객체로, `

                                    `는 `HTMLParagraphElement` 객체로 표현하며, 이런 `HTMLElement` 인터페이스를 통해서 HTML의 element를 수정할 수 있습니다. -## Key Questions +## 이벤트 흐름 제어(버블링 & 캡처링)이 무엇인가요? -- DOM은 무엇인가요? -- 이벤트 흐름 제어(버블링 & 캡처링)이 무엇인가요? -- 클로저와 스코프가 무엇인가요? +- **이벤트 버블링**이란, HTML 문서에서 이벤트가 발생했을 때, 하위 element에서부터 상위 element로 이벤트가 전달되는 방식입니다. +- **이벤트 캡처링**이란, HTML 문서에서 이벤트가 발생했을 때, 하위 element까지 이벤트가 전달되는 방식입니다. +- 이벤트는 캡처링 단계, 타깃 단계, 버블링 단계를 거칩니다. + - **캡처링 단계**에서는 이벤트가 최상위 조상(window또는 document)에서부터 하위 element로 전달됩니다. 기본적으로 캡처링 단계에서는 이벤트 핸들러가 실행되지 않지만, 이벤트 핸들러에 옵션을 주어 이 단계에서 이벤트를 핸들링할 수 있습니다. + - **타깃 단계**에서는 이벤트가 실제 타깃 요소에 전달됩니다. 이벤트가 타깃 element에 도착하면 타깃 element에 부착된 이벤트 핸들러가 동작합니다. + - **버블링 단계**에서는 이벤트가 상위 element로 전달됩니다. `focus`, `blur` 등의 일부 이벤트는 버블링 단계를 거치지 않습니다. -## 필수 요건 +## 클로저와 스코프가 무엇인가요? -- [결과 화면](https://vanilla-todo-19th-dh.vercel.app/)의 기능을 구현합니다. (날짜, 요일별 todo 개수) -- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다. -- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다. -- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다. -- 함수와 변수의 이름은 lowerCamelCase로 짓습니다. -- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다. -- Semantic tag를 활용하여 HTML 구조를 완성합니다. +- 스코프는 값과 표현식이 참조될 수 있는 컨텍스트를 의미합니다. + - 하위 스코프에서는 상위 스코프에 접근할 수 있지만, 상위 스코프에서는 하위 스코프에 접근할 수 없습니다. + - JS에는 3가지 종류의 스코프가 있습니다. + - Global scope(전역 스코프) + - Module scope(모듈 스코프) + - Function scope(함수 스코프) + - Block scope(블록 스코프) +- 클로저는 함수와 그 함수가 선언될 때의 lexical environment의 조합입니다. 함수가 생성될 때마다 클로저도 생성되며, 함수는 클로저를 통해 자신이 선언된 시점의 lexical environment(변수나 함수들)을 참조할 수 있습니다. -## 선택 요건 +## 참고 -- 외부 폰트 Pretendard를 적용합니다. -- 브라우저의 `localStorage` 혹은 `sessionStorage`를 이용하여 다음 번 접속 시에 기존의 투두 데이터를 불러옵니다. -- 이 외에도 추가하고 싶은 기능이 있다면 마음껏 추가하셔도 됩니다. -- 미디어쿼리를 이용해서 반응형을 적용합니다. - -# 링크 및 참고자료 - -- [HTML/CSS 기초](https://heropy.blog/2019/04/24/html-css-starter/) -- [HTML 태그](https://heropy.blog/2019/05/26/html-elements/) -- [FlexBox 가이드](https://heropy.blog/2018/11/24/css-flexible-box/) -- [JS를 통한 DOM 조작](https://velog.io/@bining/javascript-DOM-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0#append) -- [localStorage, sessionStorage](https://www.daleseo.com/js-web-storage/) -- [git 사용법](https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/) -- [좋은 코드리뷰 방법](https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/) -- [MDN 공식문서-createElement()](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement) -- [MDN 공식문서-appendChild()](https://developer.mozilla.org/ko/docs/Web/API/Node/appendChild) -- [DOM 개념,HTML 요소 조작](https://poiemaweb.com/js-dom#3-dom-query--traversing-%EC%9A%94%EC%86%8C%EC%97%90%EC%9D%98-%EC%A0%91%EA%B7%BC) +- https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction +- https://ko.javascript.info/bubbling-and-capturing +- https://developer.mozilla.org/en-US/docs/Glossary/Scope +- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures