diff --git a/01-first-component/README.md b/01-first-component/README.md deleted file mode 100644 index 72b1469..0000000 --- a/01-first-component/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# 01. 컴포넌트 선언하고 사용하기: Component 기본 구조와 JSX - -## 🎯 요구 사항 -- `/templates` 에 있는 html 템플릿을 그대로 `App.jsx`에서 그릴 수 있게 해보세요. -- `App.jsx`를 여러 개의 컴포넌트로 분리해서 그려보세요. - - 스타일도 별도의 css파일로 분리하여 각 컴포넌트에서 import합니다. - - (선택) `module.css` 를 사용해 보세요. - -### 구현 결과 예시 -- 예를 들어, `App.jsx`의 return문을 아래와 같이 작성했을 때에 앱이 정상적으로 그려지도록 구현해 주세요. -- 컴포넌트의 이름이나 구조는 마음대로 변경해도 좋습니다 -```javascript -function App() { - return ( - <> -
-
- - -
- - - ); -} -``` - -## ✅ 키워드 -- JSX - - `class` -> `className` - - `for` -> `htmlFor` - - self closing tag - - Fragment - - `{}` 내에 쓸 수 있는 JS 식 -- React Component - - 기본 구조 - - export / import - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step1-2 에 있는 것처럼 나만의 컴포넌트 단위를 나누어 보세요. -- [Your First Component](https://react.dev/learn/your-first-component) diff --git a/01-styled-components/README.md b/01-styled-components/README.md new file mode 100644 index 0000000..14979e9 --- /dev/null +++ b/01-styled-components/README.md @@ -0,0 +1,85 @@ +# 01. styled-components를 적용해서 리팩토링하기 + +## 🎯 요구사항 +- 초록스터디-`self-paced-react`의 step5 코드를 가져와 스타일링하는 미션입니다. +- styled-components 라이브러리를 이용해서 컴포넌트들에게 css를 입히고 스타일링 해보세요. +- styled-components를 왜 사용하는지, 별도의 css파일로 분리한 방법과 어떤 trade-off가 있는지 PR에 적어주세요. + - (선택) 브라우저가 웹 페이지를 렌더링하는 구조와 과정에 대해서도 공부해보세요. + - **🔑keywords** : DOM Tree, CSSOM Tree, Render Tree +- 프로젝트내 별도의 css파일은 존재하지 않아야합니다.❌ + - App.css는 허용되며 아래의 코드와 동일해야합니다. +```css +* { + padding: 0; + margin: 0; + box-sizing: border-box; + } + +ul, +li { +list-style: none; +} + +html, +body { +font-family: sans-serif; +font-size: 16px; +} + +/* Colors *****************************************/ +:root { +--primary-color: #ec4a0a; +--lighten-color: #f6a88a; +--grey-100: #ffffff; +--grey-200: #d0d5dd; +--grey-300: #667085; +--grey-400: #344054; +--grey-500: #000000; +} + +``` + +### 😗구현 예시 +- 컴포넌트의 이름이나 구조는 마음대로 변경해도 좋습니다. +```javascript +import React from 'react'; +import styled from 'styled-components'; + +// Button 컴포넌트를 styled-components로 정의 +const Button = styled.button` + background-color: #ec4a0a; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + + &:hover { + background-color: #f6a88a; + } +`; + +const App = () => { + return ( +
+

Hello, styled-components!

+ +
+ ); +}; + +export default App; +``` + +## ✅ 키워드 +- **styled-component** + - Css in JS + - Scoped Styling + +## 🧙‍♀️ 진행 가이드 +- 진행시간 : 1시간 내에 완료하는 것을 목표로 합니다. +- vscode 사용시 extension에서 `vscode-styled-components`를 설치해주세요. + +## 🔗 참고 문서 +- [styled-components 공식문서](https://styled-components.com/docs) \ No newline at end of file diff --git a/02-rendering-lists/README.md b/02-rendering-lists/README.md deleted file mode 100644 index 3fcfcf1..0000000 --- a/02-rendering-lists/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 02. 목록 UI 구현하기: Props와 State - -## 🎯 요구 사항 -- `RestaurantList` 가 restaurants 배열을 받아서 그릴 수 있도록 변경해 보세요. - - restaurants 배열을 `RestaurantList` 의 props로 내려받도록 변경해 보세요. -- 카테고리 필터에 따라 필터된 음식점 목록을 보여줄 수 있도록 변경해 보세요. - -### 구현 결과 예시 -```javascript -// App.jsx - - -``` -```javascript -const restaurants = [ - { - id: "a01", - name: "피양콩할마니", - description: - "평양 출신의 할머니가 수십 년간 운영해온 비지 전문점 피양콩 할마니. 두부를 빼지 않은 되비지를 맛볼 수 있는 곳으로, ‘피양’은 평안도 사투리로 ‘평양’을 의미한다. 딸과 함께 운영하는 이곳에선 맷돌로 직접 간 콩만을 사용하며, 일체의 조미료를 넣지 않은 건강식을 선보인다. 콩비지와 피양 만두가 이곳의 대표 메뉴지만, 할머니가 옛날 방식을 고수하며 만들어내는 비지전골 또한 이 집의 역사를 느낄 수 있는 특별한 메뉴다. 반찬은 손님들이 먹고 싶은 만큼 덜어 먹을 수 있게 준비돼 있다.", - category: "한식", - }, - { - id: "a02", - name: "친친", - description: "Since 2004 편리한 교통과 주차, 그리고 관록만큼 깊은 맛과 정성으로 정통 중식의 세계를 펼쳐갑니다", - category: "중식", - }, - { - id: "a03", - name: "잇쇼우", - description: - "잇쇼우는 정통 자가제면 사누끼 우동이 대표메뉴입니다. 기술은 정성을 이길 수 없다는 신념으로 모든 음식에 최선을 다하는 잇쇼우는 고객 한분 한분께 최선을 다하겠습니다", - category: "일식", - }, - { - id: "a04", - name: "이태리키친", - description: "늘 변화를 추구하는 이태리키친입니다.", - category: "양식", - }, - { - id: "a05", - name: "호아빈 삼성점", - description: "푸짐한 양에 국물이 일품인 쌀국수", - category: "아시안", - }, - { - id: "a06", - name: "도스타코스 선릉점", - description: "멕시칸 캐주얼 그릴", - category: "기타", - }, -]; -``` - - -## ✅ 키워드 -- Props -- State - - useState -- Keys - -> [Rendering Lists](https://react.dev/learn/rendering-lists) 문서에 ['Why does React need keys?'](https://react.dev/learn/rendering-lists#why-does-react-need-keys)는 지금 꼭 이해하지 않아도 괜찮습니다. 그냥 React에서 목록을 동적으로 그릴 때에는 이런 것들을 사용해야 하는구나~ 정도로만 알고 일단 넘어가세요. 우선 사용하는 법에 익숙해지는 것이 먼저입니다 :) - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step3-4 -- [Passing Props to a Component](https://react.dev/learn/passing-props-to-a-component) -- [Rendering Lists](https://react.dev/learn/rendering-lists) -- [State: A Component's Memory](https://react.dev/learn/state-a-components-memory) - - [API Reference: useState](https://react.dev/reference/react/useState) \ No newline at end of file diff --git a/03-modal/README.md b/03-modal/README.md deleted file mode 100644 index bc74460..0000000 --- a/03-modal/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# 03. 모달 UI 구현하기: side-effect(feat. event handler) - -## 🎯 요구 사항 -- `RestaurantList` 의 아이템을 클릭하면, 클릭한 아이템의 정보를 보여주는 모달이 뜨도록 변경해 주세요. '확인' 버튼을 클릭하거나 모달 뒤의 backdrop을 클릭하면 모달이 닫혀야 합니다. - - (작은 단계로 구현해보기 1) 아이템을 클릭하면 정해진 텍스트를 그대로 보여주는 모달을 열고 닫습니다. - - (작은 단계로 구현해보기 2) 클릭한 아이템의 정보를 모달에 내려줄 수 있도록 개선합니다. - -### 구현 결과 예시 -```javascript -// App.jsx -{isModalOpen && } -``` - -## ✅ 키워드 -- event handler (feat. side effect) -- conditional rendering -- lifting state up - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step5 -- [Responding to Events](https://react.dev/learn/responding-to-events) \ No newline at end of file diff --git a/04-form/README.md b/04-form/README.md deleted file mode 100644 index b029ec9..0000000 --- a/04-form/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# 04. 폼 UI 구현하기: controlled vs uncontrolled - -## 🎯 요구 사항 -- `Header`의 레스토랑 추가 버튼을 클릭하면 레스토랑 추가 폼이 모달로 뜨도록 구현해 주세요 - - 이전 단계에서 만들어두었던 `AddRestaurantModal`을 그대로 사용합니다. -- 카테고리를 선택하고, ``, ` - - 메뉴 등 추가 정보를 입력해 주세요. - - - -
- -
- - - ); -} diff --git a/src/components/AddRestaurantModal/AddRestaurantModal.module.css b/src/components/AddRestaurantModal/AddRestaurantModal.module.css deleted file mode 100644 index 58dd401..0000000 --- a/src/components/AddRestaurantModal/AddRestaurantModal.module.css +++ /dev/null @@ -1,77 +0,0 @@ -.formItem { - display: flex; - flex-direction: column; - - margin-bottom: 36px; -} - -.formItem label { - color: var(--grey-400); - font-size: 14px; -} - -.formItem--required label::after { - padding-left: 4px; - - color: var(--primary-color); - content: "*"; -} - -.formItem__helpText { - color: var(--grey-300); -} - -.formItem input, -.formItem textarea, -.formItem select { - padding: 8px; - margin: 6px 0; - - border: 1px solid var(--grey-200); - border-radius: 8px; - - font-size: 16px; -} - -.formItem textarea { - resize: none; -} - -.formItem select { - height: 44px; - - padding: 8px; - - border: 1px solid var(--grey-200); - border-radius: 8px; - - color: var(--grey-300); -} - -input[name="name"], -input[name="link"] { - height: 44px; -} - -.modal__buttonContainer { - display: flex; -} - -.button { - width: 100%; - height: 44px; - - margin-right: 16px; - - border: none; - border-radius: 8px; - - font-weight: 600; - cursor: pointer; -} - -.button--primary { - background: var(--primary-color); - - color: var(--grey-100); -} diff --git a/src/components/CategoryFilter.jsx b/src/components/CategoryFilter.jsx new file mode 100644 index 0000000..0fc45d3 --- /dev/null +++ b/src/components/CategoryFilter.jsx @@ -0,0 +1,40 @@ +import { ALL_CATEGORY, CATEGORIES } from "../constants/categories.js"; +import styled from "styled-components"; + +const CategoryFilterSection = styled.section` + display: flex; + justify-content: space-between; + padding: 0 16px; + margin-top: 24px; +`; + +const CategorySelect = styled.select` + height: 44px; + min-width: 125px; + border: 1px solid var(--grey-200); + border-radius: 8px; + background: transparent; + font-size: 16px; + padding: 8px; +`; + +export default function CategoryFilter({ category, onCategoryChange }) { + return ( + + + + {CATEGORIES.map((value) => ( + + ))} + + + ); +} diff --git a/src/components/CategoryFilter/CategoryFilter.jsx b/src/components/CategoryFilter/CategoryFilter.jsx deleted file mode 100644 index fdf1d99..0000000 --- a/src/components/CategoryFilter/CategoryFilter.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ALL_CATEGORY, CATEGORIES } from "../../constants/categories.js"; -import styles from "./CategoryFilter.module.css"; - -export default function CategoryFilter({ category, onCategoryChange }) { - return ( -
- -
- ); -} diff --git a/src/components/CategoryFilter/CategoryFilter.module.css b/src/components/CategoryFilter/CategoryFilter.module.css deleted file mode 100644 index e8b3076..0000000 --- a/src/components/CategoryFilter/CategoryFilter.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.categoryFilter { - display: flex; - justify-content: space-between; - - padding: 0 16px; - margin-top: 24px; -} - -.categoryFilter select { - height: 44px; - min-width: 125px; - - border: 1px solid #d0d5dd; - border-radius: 8px; - background: transparent; - - font-size: 16px; -} - -.categoryFilter__select { - padding: 8px; -} diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..81000dc --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,50 @@ +import styled from "styled-components"; +import addButton from "../assets/add-button.png"; + +const Gnb = styled.header` + display: flex; + justify-content: space-between; + align-items: center; + height: 64px; + padding: 0 16px; + background-color: var(--primary-color); +`; + +const GnbTitle = styled.h1` + color: var(--grey-50); + font-size: 20px; + line-height: 24px; + font-weight: 600; +`; + +const GnbButton = styled.button` + height: 40px; + border: none; + border-radius: 8px; + background: transparent; + font-size: 24px; + cursor: pointer; + + img { + display: block; + width: 40px; + height: 40px; + object-fit: contain; + } +`; + +export default function Header({ onAddButtonClick }) { + return ( + + 점심 뭐 먹지 + + + + + + ); +} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx deleted file mode 100644 index 1451cbd..0000000 --- a/src/components/Header/Header.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import styles from "./Header.module.css"; -import addButton from "../../assets/add-button.png"; - -export default function Header({ onAddButtonClick }) { - return ( -
-

점심 뭐 먹지

- -
- ); -} diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css deleted file mode 100644 index 32df2e1..0000000 --- a/src/components/Header/Header.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.gnb { - display: flex; - justify-content: space-between; - align-items: center; - height: 64px; - - padding: 0 16px; - - background-color: var(--primary-color); -} - -.gnb__title { - color: #fcfcfd; -} - -.gnb__button { - height: 40px; - - border: none; - border-radius: 8px; - background: transparent; - - font-size: 24px; - cursor: pointer; -} - -.gnb__button img { - display: block; - width: 40px; - height: 40px; - object-fit: contain; -} diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx new file mode 100644 index 0000000..a54aa22 --- /dev/null +++ b/src/components/Modal.jsx @@ -0,0 +1,47 @@ +import { useEffect } from "react"; +import styled from "styled-components"; + +const ModalBackdrop = styled.div` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, 0.35); +`; + +const ModalContainer = styled.div` + position: fixed; + bottom: 0; + width: 100%; + padding: 32px 16px; + border-radius: 8px 8px 0px 0px; + background: var(--grey-100); +`; + +const ModalTitle = styled.h2` + margin-bottom: 36px; + font-size: 20px; + line-height: 24px; + font-weight: 600; +`; + +export default function Modal({ children, title, onClose }) { + useEffect(() => { + function handleKeyDown(e) { + if (e.key === "Escape") onClose(); + } + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [onClose]); + + return ( + <> + + + {title} + {children} + + + ); +} diff --git a/src/components/Modal/Modal.jsx b/src/components/Modal/Modal.jsx deleted file mode 100644 index 7c55306..0000000 --- a/src/components/Modal/Modal.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from "react"; -import styles from "./Modal.module.css"; - -export default function Modal({ children, title, onClose }) { - useEffect(() => { - function handleKeyDown(e) { - if (e.key === "Escape") onClose(); - } - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [onClose]); - - return ( - <> -
-
-

{title}

- {children} -
- - ); -} diff --git a/src/components/Modal/Modal.module.css b/src/components/Modal/Modal.module.css deleted file mode 100644 index 0937846..0000000 --- a/src/components/Modal/Modal.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.modal__backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - background: rgba(0, 0, 0, 0.35); -} - -.modal__container { - position: fixed; - bottom: 0; - width: 100%; - - padding: 32px 16px; - - border-radius: 8px 8px 0px 0px; - background: var(--grey-100); -} - -.modal__title { - margin-bottom: 36px; -} diff --git a/src/components/RestaurantDetailModal.jsx b/src/components/RestaurantDetailModal.jsx new file mode 100644 index 0000000..fd82ba9 --- /dev/null +++ b/src/components/RestaurantDetailModal.jsx @@ -0,0 +1,43 @@ +import styled from "styled-components"; +import Modal from "./Modal.jsx"; + +const ButtonContainer = styled.div` + display: flex; +`; + +const Button = styled.button` + width: 100%; + height: 44px; + margin-right: 16px; + border: none; + border-radius: 8px; + font-size: 14px; + line-height: 20px; + font-weight: 600; + cursor: pointer; + background: var(--primary-color); + color: var(--grey-100); +`; + +const RestaurantInfo = styled.div` + margin-bottom: 24px; +`; + +const Description = styled.p` + font-size: 16px; + line-height: 24px; + font-weight: 400; +`; + +export default function RestaurantDetailModal({ restaurant, onClose }) { + return ( + + + {restaurant.description} + + + + + + ); +} diff --git a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx b/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx deleted file mode 100644 index 9318891..0000000 --- a/src/components/RestaurantDetailModal/RestaurantDetailModal.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import styles from "./RestaurantDetailModal.module.css"; -import Modal from "../Modal/Modal.jsx"; - -export default function RestaurantDetailModal({ restaurant, onClose }) { - return ( - -
-

{restaurant.description}

-
-
- -
-
- ); -} diff --git a/src/components/RestaurantDetailModal/RestaurantDetailModal.module.css b/src/components/RestaurantDetailModal/RestaurantDetailModal.module.css deleted file mode 100644 index 56deefd..0000000 --- a/src/components/RestaurantDetailModal/RestaurantDetailModal.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.modal__buttonContainer { - display: flex; -} - -.modal__restaurantInfo { - margin-bottom: 24px; -} - -.button { - width: 100%; - height: 44px; - - margin-right: 16px; - - border: none; - border-radius: 8px; - - font-weight: 600; - cursor: pointer; -} - -.button--primary { - background: var(--primary-color); - - color: var(--grey-100); -} diff --git a/src/components/RestaurantList.jsx b/src/components/RestaurantList.jsx new file mode 100644 index 0000000..26d0588 --- /dev/null +++ b/src/components/RestaurantList.jsx @@ -0,0 +1,90 @@ +import { CATEGORY_IMAGES } from "../constants/categoryImages.js"; +import styled from "styled-components"; + +const List = styled.ul` + padding: 0 16px; + margin: 16px 0; +`; + +const Restaurant = styled.li` + border-bottom: 1px solid var(--grey-150); +`; + +const Button = styled.button` + display: flex; + align-items: flex-start; + width: 100%; + padding: 16px 8px; + background: none; + border: none; + cursor: pointer; + text-align: left; +`; + +const Category = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 64px; + height: 64px; + min-width: 64px; + min-height: 64px; + margin-right: 16px; + border-radius: 50%; + background: var(--lighten-color); + + img { + width: 36px; + height: 36px; + } +`; + +const Info = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-start; +`; + +const RestaurantName = styled.h3` + margin: 0; + font-size: 18px; + line-height: 28px; + font-weight: 600; +`; + +const RestaurantDescription = styled.p` + display: -webkit-box; + padding-top: 8px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + font-size: 16px; + line-height: 24px; + font-weight: 400; +`; + +export default function RestaurantList({ restaurants, onRestaurantClick }) { + return ( + + {restaurants.map((restaurant) => { + return ( + + + + ); + })} + + ); +} diff --git a/src/components/RestaurantList/RestaurantList.jsx b/src/components/RestaurantList/RestaurantList.jsx deleted file mode 100644 index 0150f7f..0000000 --- a/src/components/RestaurantList/RestaurantList.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import styles from "./RestaurantList.module.css"; -import { CATEGORY_IMAGES } from "../../constants/categoryImages.js"; - -export default function RestaurantList({ restaurants, onRestaurantClick }) { - return ( -
-
    - {restaurants.map((restaurant) => { - return ( -
  • - -
  • - ); - })} -
-
- ); -} diff --git a/src/components/RestaurantList/RestaurantList.module.css b/src/components/RestaurantList/RestaurantList.module.css deleted file mode 100644 index f7bdbdf..0000000 --- a/src/components/RestaurantList/RestaurantList.module.css +++ /dev/null @@ -1,69 +0,0 @@ -.restaurantList { - display: flex; - flex-direction: column; - - padding: 0 16px; - margin: 16px 0; -} - -.restaurant { - border-bottom: 1px solid #e9eaed; -} - -.restaurant__button { - display: flex; - align-items: flex-start; - - width: 100%; - padding: 16px 8px; - - background: none; - border: none; - cursor: pointer; - text-align: left; -} - -.restaurant__category { - display: flex; - justify-content: center; - align-items: center; - width: 64px; - height: 64px; - min-width: 64px; - min-height: 64px; - - margin-right: 16px; - - border-radius: 50%; - background: var(--lighten-color); -} - -.restaurant__categoryIcon { - width: 36px; - height: 36px; -} - -.restaurant__info { - display: flex; - flex-direction: column; - justify-content: flex-start; -} - -.restaurant__name { - margin: 0; -} - -.restaurant__distance { - color: var(--primary-color); -} - -.restaurant__description { - display: -webkit-box; - - padding-top: 8px; - - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -}