diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..1fd70a8 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 3c3629e..a2e04e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules + +.env \ No newline at end of file diff --git a/README.md b/README.md index 8711ce4..827853a 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,29 @@ # 서론 -안녕하세요 🙌🏻 17기 프론트엔드 운영진 **유선호**입니다. -다들 1주차 미션 Vanilla Todo 만드시느라 수고 많으셨습니다! 1주차 미션을 통해 여러분들께서 본격적인 React 사용에 앞서 Vanilla JS로 SPA를 만들때의 불편한 점을 느끼셨을 것 이라 생각합니다. +안녕하세요 🙌🏻 +17기 프론트엔드 **김문기** 입니다!! -그리하여 이번 미션은, 1주차 스터의 미션으로 주어진 Todo list 만들기를 **React**로 리팩토링하는 것 입니다! -기존에 리액트를 잘 아시던 분들께는, 조금 더 효울적인 디자인 패턴에 대해 고민할수 있는 주차가 될 것이고, 리액트를 접해보지 못하신 분들께는 기존의 어플리케이션을 리액트로 포팅하는 과정을 통해 왜 프론트엔드 시장에 리액트가 등장하게 되었고, 리액트에서 사용하는 여러가지 방식들이 왜 바닐라에 비해 효율적인지 꺠닫는 주차가 될 것이라 생각합니다. +왜인지 모르겠는데 하나의 파일에 작성하는 js가 약간은 그리웠던 프로젝트입니다. +useMemo나 useCallback등의 함수를 사용해본적이 없어서 이번에 todo를 만들면서 사용해보아야겠다! +고 했던 1주일전의 저와는 다르게 아무것도 사용하지 ~~않았~~ 못했습니다 -비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 **창의성**을 충분히 발휘해보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 많이 고민해보시고, 본인이 작성할 수 있는 가장 창의적인 방법으로 코드를 작성해주셨으면 합니다. 여러분이 미션 수행을 하는 과정에서 한 생각과 고민만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다. +!!! 사용법 !!! +구현이 완벽하지 않아서 사용법을 적어놓겠습니다 ㅜㅜㅜㅜ +- 바탕화면의 아이콘을 한번 누르면 아이콘이 focus됩니다 +- 바탕화면의 아이콘을 더블클릭하면 창이 열리거나 닫힙니다 +- 이후는 지난주와 동일합니다! +~~모바일에서는 더블클릭이 안된다고 하더라고요... 모바일은 다음부터 기능 구현해보겠습니다😢~~ +데스크탑의 모바일뷰로는 안되는데 핸드폰으로는 잘 되네요 ㅎㅎ -막히는 부분이 있더라도 우선 스스로 공부하고 찾아보면서 미션을 진행하는 방식을 권고드리지만, 미션과 관련하여 운영진의 도움이 필요하시다면 얼마든지 슬랙 Q&A 채널이나 프론트엔드 카톡방에 질문을 남겨 주세요! +추가 구현하고 싶은 기능들이 많았는데, 체력과 시간 이슈로 저번주 과제를 react로 옮기는 것에 집중하기로했습니다 ㅜㅜ +- swipe기능 +- drag and drop 기능 +의 기능을 시도해보고 싶은데, 관심 있으신 분들은 함께 얘기해보면 좋을 것 같아요! # 미션 -## 예시 - -- [리액트 투두](https://react-todo-16th-kongnayeon.vercel.app/) - ## 미션 목표 - VSCode, Prettier를 이용하여 개발환경을 관리합니다. @@ -34,13 +40,19 @@ ## Key Questions - Virtual-DOM은 무엇이고, 이를 사용함으로서 얻는 이점은 무엇인가요? +-> Document Model 인데 Virtual하다. 즉, 가상의 Document Model으로, DOM에 state가 업데이트 될 경우, update되는 state의 갯수 만큼 DOM이 렌더링이 되어야 하는데, Virtual Dom을 사용하면 state가 update되는것을 기다렸다가, DOM이 새로이 생성되기 전, 이전 상태 값과 현재의 update값을 비교하여 '달라진 부분만'을 DOM에 전달하여 한번의 렌더링을 진행할 수 있게 됩니다. + - 미션을 진행하면서 느낀, React를 사용함으로서 얻을수 있는 장점은 무엇이었나요? +-> 앞으로 자주 쓸 혹은 자주 쓰는 구조의 경우 Component로 따로 제작하여 반복 호출할 수 있음. 각각의 page나 component에서 필요한 js를 나누어 사용함으로서 코드 관리가 용이하다. 정도를 느꼈습니다. + - React에서 상태란 무엇이고 어떻게 관리할 수 있을까요? +-> React에서 상태는 data를 hold하고 있는 객체로, 컴포넌트의 현재 상태를 저장하고 있는 공간정도로 생각할 수 있을 것 같습니다. 상태 관리를 할 때 react에서는 useState에 자동으로 제공되는 get, set함수를 사용하기도 하지만, 프로젝트 폴더 전체에서 효율적으로 상태 관리를 하기 위해 `context-api` `redux` `recoil`등의 외부 라이브러리를 사용하기도 합니다. - Styled-Components 사용 후기 (CSS와 비교) +-> css와 비교할 때 css에서는 태그에 className을 열거하여 해당 className이 있는 경우 다른 스타일을 주는 식으로 해야했지만, styled-component를 사용하면 props를 css로 넘겨 css안에서 props의 값에 대한 렌더링을 할 수 있습니다. 또한, 가상 클래스 선택자 또한 하나의 블락(`)안에 작성함으로서 보다 깔끔한 코드 작성이 가능합니다. ## 필수 요건 -- 1주차 미션의 결과물을 그대로 React로 구현합니다 +- 1주차 미션의 결과물을 **그대로** React로 구현합니다 - Functional Components를 사용합니다 - React Hooks만을 사용해 상태를 관리합니다 - (이번주는 Redux, MobX, Recoil, SWR등의 외부 상태관리 라이브러리를 사용하지 않아도 미션 수행에 지장이 없습니다.) @@ -49,21 +61,8 @@ - 기존 Todo-list에 여러분들이 추가하고 싶은 기능과 디자인을 자유롭게 추가해보세요. -## 로컬 실행방법 - ---- - -`npm start` : 로컬에서 react application을 자동으로 리로드하여 실행시켜줍니다. - - # 링크 및 참고자료 ---- +[Vercel 배포 링크](https://react-todo-17th-seven.vercel.app/) -- [create react app (CRA)](https://create-react-app.dev/docs/getting-started/) -- [리액트 docs 주요 개념 1-12](https://ko.reactjs.org/docs/hello-world.html) -- [리액트 docs Hook 1-3](https://ko.reactjs.org/docs/hooks-intro.html) -- [리액트 useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/) -- [컴포넌트 네이밍을 위한 자바스크립트 네이밍 컨벤션](https://velog.io/@cada/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%8A%A4%ED%83%80%EC%9D%BC-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%84%A4%EC%9D%B4%EB%B0%8D-%EC%BB%A8%EB%B2%A4%EC%85%98-%ED%8E%B8) -- [useState, useEffect hooks](https://velog.io/@velopert/react-hooks#1-usestate) -- [styled-component](https://styled-components.com/docs/basics#getting-started) \ No newline at end of file +[진행하면서 적은 내용](https://moong23.notion.site/2-React-todo-bc0cb4a2a2104dcaa6a8986a08601186) diff --git a/package-lock.json b/package-lock.json index eb6e658..b56b02b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,10 @@ "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.5", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^5.3.9", "web-vitals": "^2.1.4" } }, @@ -2131,6 +2134,29 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "dependencies": { + "@emotion/memoize": "^0.8.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz", @@ -5314,6 +5340,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "node_modules/babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", @@ -5627,6 +5673,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -5780,6 +5834,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6058,6 +6120,14 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -6239,6 +6309,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -8618,6 +8698,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8710,6 +8795,19 @@ "he": "bin/he" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -14333,6 +14431,19 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz", + "integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -14455,6 +14566,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15129,6 +15259,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15531,6 +15666,35 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -16064,16 +16228,16 @@ } }, "node_modules/typescript": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", - "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -18598,6 +18762,29 @@ "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", "requires": {} }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@eslint-community/eslint-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz", @@ -20949,6 +21136,23 @@ "@babel/helper-define-polyfill-provider": "^0.3.3" } }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", @@ -21193,6 +21397,11 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -21299,6 +21508,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -21513,6 +21727,11 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -21620,6 +21839,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -23366,6 +23595,11 @@ "duplexer": "^0.1.2" } }, + "hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -23425,6 +23659,21 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -27303,6 +27552,15 @@ "scheduler": "^0.23.0" } }, + "react-draggable": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz", + "integrity": "sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==", + "requires": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + } + }, "react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -27399,6 +27657,14 @@ "picomatch": "^2.2.1" } }, + "recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "requires": { + "hamt_plus": "1.0.2" + } + }, "recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -27883,6 +28149,11 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -28186,6 +28457,23 @@ "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", "requires": {} }, + "styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, "stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -28588,9 +28876,9 @@ } }, "typescript": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz", - "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true }, "unbox-primitive": { diff --git a/package.json b/package.json index 101e3ed..d58e4a0 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", + "recoil": "^0.7.7", + "styled-components": "^5.3.9", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/public/Link.cur b/public/Link.cur new file mode 100644 index 0000000..0da8f98 Binary files /dev/null and b/public/Link.cur differ diff --git a/public/Text.cur b/public/Text.cur new file mode 100644 index 0000000..92b5420 Binary files /dev/null and b/public/Text.cur differ diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..cebe10b 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/icon_100.png b/public/icon_100.png new file mode 100644 index 0000000..7497bc3 Binary files /dev/null and b/public/icon_100.png differ diff --git a/public/index.html b/public/index.html index aa069f2..fdebe5c 100644 --- a/public/index.html +++ b/public/index.html @@ -9,7 +9,7 @@ name="description" content="Web site created using create-react-app" /> - + - React App + MY_TODO diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..be66638 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,21 +1,11 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "MY_TODO", + "name": "Simple Todo Planner", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..8d1fa56 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/App.js b/src/App.js index 7c4c71a..e36e06b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,38 @@ +import { useEffect, useState } from "react"; +import Mainpage from "./pages/Mainpage/Mainpage"; + function App() { - return ( -
-

17기 프론트 화이팅~ 우하하

-
+ const [clickedIcon, setClickedIcon] = useState(""); + const [mainIconPosition, setMainIconPosition] = useState( + JSON.parse(localStorage.getItem("mainIconPosition")) ?? { + top: 30, + left: 30, + } + ); + const [textIconPosition, setTextIconPosition] = useState( + JSON.parse(localStorage.getItem("textIconPosition")) ?? { + top: 160, + left: 30, + } ); + + useEffect(() => { + if (!localStorage.getItem("mainIconPosition")) { + localStorage.setItem( + "mainIconPosition", + JSON.stringify(mainIconPosition) + ); + } + if (!localStorage.getItem("textIconPosition")) { + localStorage.setItem( + "textIconPosition", + JSON.stringify(textIconPosition) + ); + } + }, [mainIconPosition, textIconPosition]); + + useEffect(() => {}); + return ; } -export default App; \ No newline at end of file +export default App; diff --git a/src/assets/.DS_Store b/src/assets/.DS_Store new file mode 100644 index 0000000..93ca2a5 Binary files /dev/null and b/src/assets/.DS_Store differ diff --git a/src/assets/Link.cur b/src/assets/Link.cur new file mode 100644 index 0000000..0da8f98 Binary files /dev/null and b/src/assets/Link.cur differ diff --git a/src/assets/Normal.cur b/src/assets/Normal.cur new file mode 100644 index 0000000..cef08b8 Binary files /dev/null and b/src/assets/Normal.cur differ diff --git a/src/assets/background_image.jpg b/src/assets/background_image.jpg new file mode 100644 index 0000000..18e049e Binary files /dev/null and b/src/assets/background_image.jpg differ diff --git a/src/assets/icon_main.png b/src/assets/icon_main.png new file mode 100644 index 0000000..cd50271 Binary files /dev/null and b/src/assets/icon_main.png differ diff --git a/src/assets/icon_text.png b/src/assets/icon_text.png new file mode 100644 index 0000000..7ec9ffb Binary files /dev/null and b/src/assets/icon_text.png differ diff --git a/src/components/card/Card.element.js b/src/components/card/Card.element.js new file mode 100644 index 0000000..01483ab --- /dev/null +++ b/src/components/card/Card.element.js @@ -0,0 +1,92 @@ +import styled from "styled-components"; + +export const CardContainer = styled.div` + width: ${(props) => (props.display ? "max(300px, 30vw)" : "0")}; + height: ${(props) => (props.display ? "max(500px, 50vw)" : "0")}; + position: ${(props) => (props.display ? "initial" : "fixed")}; + top: ${(props) => props.position.top}; + left: ${(props) => props.position.left}; + margin: 0 auto; + background-color: #1e1e1e; + border-radius: 12px; + z-index: 2; + visibility: ${(props) => (props.display ? "visible" : "hidden")}; + box-shadow: 0 0 10px 0px #000000; + transition: width 0.2s, height 0.2s, visibility 0.05s linear 0.15s; +`; + +export const CardToolBar = styled.div` + display: flex; + align-items: center; + height: 40px; + padding: 12px; + background-color: #312f34; + border-radius: 12px 12px 0 0; + box-sizing: border-box; + position: relative; + visibility: ${(props) => (props.display ? "visible" : "hidden")}; +`; + +export const CardPlusBtn = styled.div` + width: 20px; + height: 20px; + color: white; + display: flex; + justify-content: center; + font-size: 24px; + border-radius: 4px; + position: absolute; + right: 12px; + padding: 2px; + &:hover { + background-color: #656565; + cursor: url(Link.cur) 0 0, pointer; + /* cursor: url(../../assets/Link.cur) 0 0, pointer; */ + } + &::before { + content: "+"; + } +`; + +export const NotHoverDiv = styled.div` + opacity: ${(props) => (props.hoverRender ? 0 : 1)}; + display: ${(props) => (props.render ? "flex" : "none")}; + flex-direction: column; + height: 100%; + transition: opacity 0.1s; +`; + +export const CardMainDiv = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(100% - 40px); + border-radius: 0 0 12px 12px; + position: relative; +`; + +export const CardTodoText = styled.h1` + color: white; + font-weight: 800; + font-size: 20px; + display: flex; + width: 100%; + text-align: left; + box-sizing: border-box; + padding: 8px 0 0 12px; + margin-bottom: 8px; +`; + +export const CardTodoDiv = styled.div` + display: flex; + flex-wrap: wrap; + height: 100%; + flex-direction: row; + justify-content: center; + overflow-y: auto; + position: relative; + align-content: flex-start; + overflow-x: hidden; + width: 100%; +`; diff --git a/src/components/card/Card.js b/src/components/card/Card.js new file mode 100644 index 0000000..9c324a5 --- /dev/null +++ b/src/components/card/Card.js @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react"; +import CircleBtn from "../circleBtn/CircleBtn"; + +import HoverDiv from "../hoverDiv/HoverDiv"; +import Todo from "../todo/Todo"; +import { + CardContainer, + CardToolBar, + CardPlusBtn, + CardMainDiv, + CardTodoDiv, + CardTodoText, + NotHoverDiv, +} from "./Card.element"; + +const Card = ({ render, setRender }) => { + useEffect(() => { + setHoverDivRender(false); + }, [render]); + const [hoverDivRender, setHoverDivRender] = useState(false); + + const [allList, setAllList] = useState([]); + const todoList = allList ? allList.filter((item) => item.done === false) : []; + const doneList = allList ? allList.filter((item) => item.done === true) : []; + + useEffect(() => { + setAllList(JSON.parse(localStorage.getItem("todoList")) || []); + }, [hoverDivRender]); + return ( + + + + + + setHoverDivRender(!hoverDivRender)} /> + + + + + TODO [{todoList.length}개] + + {todoList.length > 0 && + todoList.map((item) => { + return ( + + ); + })} + {todoList.length === 0 && } + + DONE [{doneList.length}개] + + {doneList.length > 0 && + doneList.map((item) => { + return ( + + ); + })} + {doneList.length === 0 && } + + + + + ); +}; + +export default Card; diff --git a/src/components/circleBtn/CircleBtn.element.js b/src/components/circleBtn/CircleBtn.element.js new file mode 100644 index 0000000..4b2336b --- /dev/null +++ b/src/components/circleBtn/CircleBtn.element.js @@ -0,0 +1,33 @@ +import styled from "styled-components"; + +export const CardCircle = styled.div` + display: inline-block; + margin: 0 4px; + align-items: center; + width: 10px; + height: 10px; + padding: 1px; + border-radius: 50%; + background-color: ${(props) => `var(--${props.color}-${props.type})`}; + &:hover { + cursor: url(Link.cur) 0 0, pointer; + } +`; + +export const InputCircle = styled.input` + appearance: none; + display: inline-block; + margin: 0 4px; + align-items: center; + width: max(12px, 1.2vw); + height: max(12px, 1.2vw); + padding: 1px; + border-radius: 50%; + background-color: ${(props) => `var(--${props.color}-${props._type})`}; + &:hover { + cursor: url(Link.cur) 0 0, pointer; + } + border: ${(props) => + props.selectedTag === props.value ? "1px solid white" : "none"}; + transition: border 0.3s ease-in-out; +`; diff --git a/src/components/circleBtn/CircleBtn.js b/src/components/circleBtn/CircleBtn.js new file mode 100644 index 0000000..d02116c --- /dev/null +++ b/src/components/circleBtn/CircleBtn.js @@ -0,0 +1,39 @@ +import { CardCircle, InputCircle } from "./CircleBtn.element"; + +const CircleBtn = ({ + color, + type, + render, + setRender, + name, + value, + selectedTag, + handleTagChange, +}) => { + if (type === "tag") { + // using type as _type since 'type' is reserved word in input + return ( + + ); + } else if (color === "red" && type === "button") { + return ( + setRender(!render)} + /> + ); + } else { + return ; + } +}; + +export default CircleBtn; diff --git a/src/components/hoverDiv/HoverDiv.element.js b/src/components/hoverDiv/HoverDiv.element.js new file mode 100644 index 0000000..2897d80 --- /dev/null +++ b/src/components/hoverDiv/HoverDiv.element.js @@ -0,0 +1,82 @@ +import styled, { keyframes } from "styled-components"; +const slideIn = keyframes` + from { + transform: translateY(-10%); + } + to { + transform: translateY(0); + } +`; +export const HoverDivContainer = styled.form` + position: absolute; + visibility: ${(props) => + props.hoverDivRender === true ? "visible" : "hidden"}; + opacity: ${(props) => (props.hoverDivRender === true ? 1 : 0)}; + transform: ${(props) => + props.hoverDivRender === true ? "translateY(0)" : "translateY(-10%)"}; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 20vw; + height: 20vw; + min-width: 200px; + min-height: 200px; + border: 1px solid rgb(132, 132, 132); + border-radius: 8px; + background-color: #18181a; + color: white; + justify-content: space-evenly; + transition: opacity 0.3s ease-in-out; + transition: transform 0.3s ease-out; + animation: ${slideIn} 0.3s; + z-index: 10; +`; + +export const HoverTitle = styled.h1` + font-size: max(24px, 3vw); + font-weight: 500; + font-style: italic; + margin-bottom: 8px; +`; + +export const HoverInput = styled.input` + width: 80%; + height: max(28px, 2.5vw); + border-radius: 4px; + box-sizing: border-box; + border: none; + padding: 0 8px; + color: white; + box-shadow: inset -1px -1px 2px 0px #656565; + background-color: #2c2830; + font-size: max(12px, 1.2vw); + &:focus { + outline: none; + } + &:hover { + cursor: url(Text.cur) 0 0, text; + } +`; + +export const HoverTagDiv = styled.div` + display: flex; + flex-direction: row; + width: 75%; + justify-content: space-between; +`; + +export const HoverButton = styled.button` + width: 40%; + height: max(24px, 2.2vw); + border: none; + border-radius: 4px; + background-color: #2c2830; + box-shadow: inset -1px -1px 2px 0px #656565; + color: white; + font-size: max(12px, 1.2vw); + font-weight: 500; + margin-top: 8px; + cursor: url(Link.cur) 0 0, pointer; +`; diff --git a/src/components/hoverDiv/HoverDiv.js b/src/components/hoverDiv/HoverDiv.js new file mode 100644 index 0000000..2e18ef1 --- /dev/null +++ b/src/components/hoverDiv/HoverDiv.js @@ -0,0 +1,130 @@ +import { useEffect, useRef, useState } from "react"; +import CircleBtn from "../circleBtn/CircleBtn"; +import { + HoverButton, + HoverDivContainer, + HoverInput, + HoverTagDiv, + HoverTitle, +} from "./HoverDiv.element"; + +const HoverDiv = ({ hoverDivRender, setHoverDivRender }) => { + const [todoValue, setTodoValue] = useState(""); + const [selectedTag, setSelectedTag] = useState("red"); + + const todoInput = useRef(); + + useEffect(() => { + setTodoValue(""); + setSelectedTag("red"); + todoInput.current.focus(); + }, [hoverDivRender]); + + const handleTodoAdd = (todoValue, selectedTag) => { + const todoList = JSON.parse(localStorage.getItem("todoList")) ?? []; + const newTodo = { + id: new Date(), + content: todoValue, + tag: selectedTag, + done: false, + }; + todoList.push(newTodo); + localStorage.setItem("todoList", JSON.stringify(todoList)); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (!checkInputValidation(todoValue)) { + alert("INVALID TODO INPUT"); + todoInput.current.focus(); + } else { + // success on submit + handleTodoAdd(todoValue, selectedTag); + setHoverDivRender(false); + } + }; + const handleTodoChange = (e) => { + setTodoValue(e.target.value); + }; + const handleTagChange = (e) => { + setSelectedTag(e.target.value); + }; + const checkInputValidation = (inputstring) => { + inputstring = inputstring.trim(); + inputstring = inputstring.replace(/\s+/g, " "); + if (inputstring === "") { + return false; + } + return true; + }; + + return ( + + TODO + + + + + + + + + + 추가 + + ); +}; + +export default HoverDiv; diff --git a/src/components/mainIcon/MainIcon.element.js b/src/components/mainIcon/MainIcon.element.js new file mode 100644 index 0000000..7eee4a5 --- /dev/null +++ b/src/components/mainIcon/MainIcon.element.js @@ -0,0 +1,37 @@ +import styled from "styled-components"; + +export const MainIconContainer = styled.div` + width: 76px; + display: flex; + flex-direction: column; + position: absolute; + top: ${(props) => `${props.position.top}px`}; + left: ${(props) => `${props.position.left}px`}; + align-items: center; +`; + +export const MainIconImgDiv = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 76px; + height: 76px; + box-sizing: border-box; + border: ${(props) => (props.clicked ? "2px solid rgb(176 176 176)" : "none")}; + border-radius: 6px; + background-color: ${(props) => (props.clicked ? "rgb(0, 0, 0, 0.5)" : "")}; +`; +export const MainIconImg = styled.img` + width: 70px; + object-fit: cover; +`; + +export const MainIconSubText = styled.p` + font-size: 18px; + display: flex; + padding: 1px 4px; + justify-content: center; + margin-top: 4px; + border-radius: 4px; + background-color: ${(props) => (props.clicked ? "#2A62D9" : "")}; +`; diff --git a/src/components/mainIcon/MainIcon.js b/src/components/mainIcon/MainIcon.js new file mode 100644 index 0000000..3a30ecf --- /dev/null +++ b/src/components/mainIcon/MainIcon.js @@ -0,0 +1,30 @@ +import icon_main from "../../assets/icon_main.png"; +import icon_text from "../../assets/icon_text.png"; + +import { + MainIconContainer, + MainIconImg, + MainIconImgDiv, + MainIconSubText, +} from "./MainIcon.element"; + +const MainIcon = ({ name, position, clickedIcon }) => { + const RenderIcon = ({ iconSrc }) => { + return ( + + + + + {name} + + ); + }; + + if (name === "main") { + return ; + } else { + return ; + } +}; + +export default MainIcon; diff --git a/src/components/todo/Todo.element.js b/src/components/todo/Todo.element.js new file mode 100644 index 0000000..146d9da --- /dev/null +++ b/src/components/todo/Todo.element.js @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +export const TodoContainer = styled.div` + display: flex; + flex-direction: column; + width: 28vw; + height: 60px; + min-width: 280px; + background-color: #323232; + margin: 5px 0; + border-radius: 8px; + color: #aeaeae; + box-sizing: border-box; + padding: 10px; + transform: ${(props) => + props.toggleleft + ? "translateX(-20%)" + : props.toggleright + ? "translateX(20%)" + : "translateX(0)"}; + transition: transform 0.3s; +`; + +export const TodoTitle = styled.div` + display: block; + width: 90%; + min-height: 24px; + white-space: nowrap; + overflow-x: hidden; + overflow-y: hidden; + text-overflow: ellipsis; + font-size: 20px; + font-weight: 800; +`; + +export const TodoTime = styled.div` + font-size: 14px; + align-self: flex-end; + font-weight: 600; + color: ${(props) => `var(--${props.tag}-tag)`}; +`; diff --git a/src/components/todo/Todo.js b/src/components/todo/Todo.js new file mode 100644 index 0000000..939c5cb --- /dev/null +++ b/src/components/todo/Todo.js @@ -0,0 +1,165 @@ +import { useEffect, useRef, useState } from "react"; +import { TodoContainer, TodoTime, TodoTitle } from "./Todo.element"; + +const Todo = ({ id, content, tag, done, setAllList }) => { + const [toggleLeft, setToggleLeft] = useState(false); + const [toggleRight, setToggleRight] = useState(false); + const [timeString, setTimeString] = useState(""); + + const todoContainerDiv = useRef(); + const todoTitleDiv = useRef(); + const todoTimeDiv = useRef(); + + const calculateTime = (timestring) => { + const timeNum = (Date.now() - new Date(timestring)) / 60000; + const days = Math.floor(timeNum / 1440); + const hours = Math.floor((timeNum % 1440) / 60); + const minutes = Math.floor(timeNum % 60); + + let parts = []; + if (days > 0) parts.push(`${days}d`); + if (hours % 24 > 0) parts.push(`${hours % 24}h`); + if (minutes > 0) parts.push(`${minutes}m`); + + setTimeString(parts.length > 0 ? parts.join(" ") : "just now"); + }; + + useEffect(() => { + calculateTime(id); + const timer = setInterval(() => { + calculateTime(id); + }, 60000); + return () => { + clearInterval(timer); + }; + }, [id]); + + const handleTodoClick = (e) => { + const containerWidth = todoContainerDiv.current.offsetWidth; + let clickPosition = e.nativeEvent.offsetX; + // 시간 클릭한 경우 + if (e.target === todoTimeDiv.current) { + // 이미 오른쪽 toggle 되어 있으면 + if (toggleRight) { + let currentTodo = JSON.parse(localStorage.getItem("todoList")); + let targetData = currentTodo.find((todo) => todo.id === id); + targetData.done = true; + + console.log(currentTodo); + localStorage.setItem("todoList", JSON.stringify(currentTodo)); + setAllList(currentTodo); + // toggle 안되어 있으면 state 초기화 후 오른쪽으로 toggle + } else { + if (toggleLeft) { + setToggleLeft(false); + } else { + setToggleRight(true); + } + } + } + // 제목 클릭한 경우 + else { + if (e.target === todoTitleDiv.current) { + clickPosition += 10; + } + // 전체 영역 클릭 handling + if (clickPosition < containerWidth * 0.4) { + if (toggleRight) { + setToggleRight(false); + } else { + setToggleLeft(true); + } + if (toggleLeft) { + let currentTodo = JSON.parse(localStorage.getItem("todoList")); + let targetData = currentTodo.filter((todo) => todo.id !== id); + localStorage.setItem("todoList", JSON.stringify(targetData)); + setAllList(targetData); + } + } else if (clickPosition > containerWidth * 0.6) { + if (toggleLeft) { + setToggleLeft(false); + } else if (toggleRight) { + let currentTodo = JSON.parse(localStorage.getItem("todoList")); + let targetData = currentTodo.find((todo) => todo.id === id); + targetData.done = true; + + console.log(currentTodo); + localStorage.setItem("todoList", JSON.stringify(currentTodo)); + setAllList(currentTodo); + // toggle 안되어 있으면 state 초기화 후 오른쪽으로 toggle + } else { + setToggleRight(true); + } + } + } + }; + + const handleDoneClick = (e) => { + const containerWidth = todoContainerDiv.current.offsetWidth; + let clickPosition = e.nativeEvent.offsetX; + if (e.target === todoTimeDiv.current) { + if (toggleLeft) { + setToggleLeft(false); + } + } else { + if (e.target === todoTitleDiv.current) { + clickPosition += 10; + } + if (clickPosition < containerWidth * 0.4) { + if (!toggleLeft) { + setToggleLeft(true); + } else { + let currentTodo = JSON.parse(localStorage.getItem("todoList")); + let targetData = currentTodo.find((todo) => todo.id === id); + targetData.done = false; + console.log(currentTodo); + localStorage.setItem("todoList", JSON.stringify(currentTodo)); + setAllList(currentTodo); + } + } else if (clickPosition > containerWidth * 0.6) { + if (toggleLeft) { + setToggleLeft(false); + } + } + } + }; + if (tag !== -1 && tag !== -2) { + return ( + + {content} + + {timeString} + + + ); + } else { + if (tag === -1) { + return ( + + PRESS + ON THE TOP BAR + to add todo + + ); + } else { + return ( + <> + + CLICK LEFT TWICE + to delete todo + + + CLICK RIGHT TWICE + to complete todo + + + ); + } + } +}; + +export default Todo; diff --git a/src/index.js b/src/index.js index 7173ce5..3bf5847 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,20 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; +import React from "react"; +import * as ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles/reset.css"; +import "./styles/colors.css"; +import { RecoilRoot } from "recoil"; -ReactDOM.render( - +// ReactDOM.render( +// // +// , +// // +// document.getElementById("root") +// ); + +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + - , - document.getElementById('root') -); \ No newline at end of file + +); diff --git a/src/pages/Mainpage/Mainpage.element.js b/src/pages/Mainpage/Mainpage.element.js new file mode 100644 index 0000000..63c83ef --- /dev/null +++ b/src/pages/Mainpage/Mainpage.element.js @@ -0,0 +1,11 @@ +import styled from "styled-components"; + +export const MainpageContainer = styled.div` + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background-image: url(${(props) => props.src}); + background-size: cover; +`; diff --git a/src/pages/Mainpage/Mainpage.js b/src/pages/Mainpage/Mainpage.js new file mode 100644 index 0000000..3a6218f --- /dev/null +++ b/src/pages/Mainpage/Mainpage.js @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; + +import MainIcon from "../../components/mainIcon/MainIcon"; + +import background_image from "../../assets/background_image.jpg"; +import { MainpageContainer } from "./Mainpage.element"; +import Card from "../../components/card/Card"; + +const Mainpage = ({ clickedIcon, setClickedIcon }) => { + const [mainIconPosition, setMainIconPosition] = useState({ + top: 30, + left: 30, + }); + const [textIconPosition, setTextIconPosition] = useState({ + top: 160, + left: 30, + }); + + useEffect(() => { + setMainIconPosition({ + top: 30, + left: 30, + }); + setTextIconPosition({ + top: 160, + left: 30, + }); + }, []); + + const [mainRender, setMainRender] = useState(false); + const [textRender, setTextRender] = useState(false); + const resetClickedIcon = () => setClickedIcon(""); + + const onClickHandler = (e) => { + // 클릭 한번 한 경우 + if (e.detail === 1) { + // 클릭한 위치가 아이콘의 위치 안에 있는 경우 + // 아이콘 클릭 state토글 + // 아이콘 위치 밖 클릭 -> clickedIcon state 초기화 + if ( + e.pageX > mainIconPosition.left && + e.pageX < mainIconPosition.left + 76 && + e.pageY > mainIconPosition.top && + e.pageY < mainIconPosition.top + 100 + ) { + if (clickedIcon === "main") { + resetClickedIcon(); + } else { + setClickedIcon("main"); + } + } else if ( + e.pageX > textIconPosition.left && + e.pageX < textIconPosition.left + 76 && + e.pageY > textIconPosition.top && + e.pageY < textIconPosition.top + 100 + ) { + if (clickedIcon === "text") { + resetClickedIcon(); + } else { + setClickedIcon("text"); + } + } else { + resetClickedIcon(); + } + } else { + // 더블 클릭한 경우 -> page open + if ( + e.pageX > mainIconPosition.left && + e.pageX < mainIconPosition.left + 76 && + e.pageY > mainIconPosition.top && + e.pageY < mainIconPosition.top + 100 + ) { + // setMainRender(true); + setMainRender(!mainRender); + } else if ( + e.pageX > textIconPosition.left && + e.pageX < textIconPosition.left + 76 && + e.pageY > textIconPosition.top && + e.pageY < textIconPosition.top + 100 + ) { + setTextRender(!textRender); + alert("WIP"); + } + } + }; + // onClickHandler 끝 + + // TODO: react-draggable로 아이콘 위치 변경 + // -> position을 draggable 조건에 맞게 변경해야할듯 + return ( + + + + + + ); +}; + +export default Mainpage; diff --git a/src/states/atoms.js b/src/states/atoms.js new file mode 100644 index 0000000..fea3911 --- /dev/null +++ b/src/states/atoms.js @@ -0,0 +1,22 @@ +import { atom } from "recoil"; + +export const mainIconPositionState = atom({ + key: "mainIconPositionState", + default: JSON.parse(localStorage.getItem("mainIconPosition")) ?? { + top: 30, + left: 30, + }, +}); + +export const textIconPositionState = atom({ + key: "textIconPositionState", + default: JSON.parse(localStorage.getItem("textIconPosition")) ?? { + top: 160, + left: 30, + }, +}); + +export const clickedIconState = atom({ + key: "clickedIconState", + default: "", +}); diff --git a/src/styles/colors.css b/src/styles/colors.css new file mode 100644 index 0000000..276b0f2 --- /dev/null +++ b/src/styles/colors.css @@ -0,0 +1,11 @@ +:root { + --red-button: #ff605c; + --yellow-button: #ffbd44; + --green-button: #00ca4e; + --red-tag: #e84b31; + --blue-tag: #3476e1; + --orange-tag: #de8b32; + --yellow-tag: #e5bd3f; + --green-tag: #56b83d; + --purple-tag: #9e54ba; +} diff --git a/src/styles/reset.css b/src/styles/reset.css new file mode 100644 index 0000000..edecb1a --- /dev/null +++ b/src/styles/reset.css @@ -0,0 +1,134 @@ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; + user-select: none; + cursor: url(../assets/Normal.cur) 0 0, pointer; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +::-webkit-scrollbar { + width: 4px; +} +::-webkit-scrollbar-thumb { + background-color: #5e5e5e; + border-radius: 4px; +}