FE-SPRINT-REACT-CUSTOM-COMPONENT

잡초·2023년 4월 20일
0

Module

import { useState } from 'react';
import styled from 'styled-components';

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  position : relative;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  top:0;
  left:0;
  right:0;
  bottom:0;
  margin:0;
  background-color: rgba(0,0,0,0.4);
  z-index:1;
`;

export const ModalBtn = styled.button`
  background-color: var(--coz-purple-600);
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: 'dialog',
}))`
  // TODO : Modal창 CSS를 구현합니다.
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border-radius: 1rem;
  background-color: #fff;
  width: 500px;
  height: 200px;
  border: 3px solid lightblue;
  >.close-btn{
    position: absolute;
    top:2px;
    right:7px;
    cursor: pointer;
  }
 
`;
export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = (e) => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen)
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}
        // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        >
          {isOpen ? 'Opened' : 'Open Modal'}
          {/* open, close condition */}

          {/* TODO : 조건부 렌더링을 활용해서 Modal이 
          열린 상태(isOpen이 true인 상태)일 때는 
          ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 
          닫힌 상태(isOpen이 false인 상태)일 때는 
          ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
        </ModalBtn>

        {isOpen === false ? null :
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView onClick={e => e.stopPropagation()}>
              <div className='close-btn' onClick={openModalHandler}>&times;</div>
              <div>Hello</div>
              <div>World</div>
            </ModalView>
          </ModalBackdrop>
        }
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 
        열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
      </ModalContainer>
    </>
  );
};

Toggle

import { useState } from 'react';
import styled from 'styled-components';

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;

  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;
    background-color: #8b8b8b;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    background-position: right;
      background: linear-gradient(to left, #8b8b8b 50%, blue 50%) right;
      background-size: 200%;
      transition: 1s;
    &.toggle--checked{
      background-position: left;
      background: linear-gradient(to right, blue 50%, #8b8b8b 50%) left;
      background-size: 200%;
      transition: 1s;
    }
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.

    transition: 0.5s;
    &.toggle--checked {
    left: 27px;
    transition: 0.5s;
    }


  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  margin-top: 0.5rem;
`;

export const Toggle = () => {
  const [isOn, setIsOn] = useState(false);

  const toggleHandler = () => {
    // TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
    setIsOn(!isOn)
  };

  return (
    <>
      <ToggleContainer
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={toggleHandler}>
        {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
        {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
        <div className={`toggle-container ${isOn ? 'toggle--checked' : null}`} />
        <div />
        <div className={`toggle-circle ${isOn ? 'toggle--checked' : null}`} />
        <div />
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
      {isOn ?
        <Desc><div className='switch--On'>Toggle Switch On</div></Desc> :
        <Desc><div className='switch--Off'>Toggle Switch Off</div></Desc>
      }
    </>
  );
};

Tab

import { useState } from 'react';
import styled from 'styled-components';

// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.

const TabMenu = styled.ul`
  background-color: #dcdcdc;
  color: rgba(73, 73, 73, 0.5);
  font-weight: bold;
  display: flex;
  flex-direction: row;
  justify-items: center;
  align-items: center;
  list-style: none;
  margin-bottom: 7rem;
  height: 40px;

  .submenu {
    ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}

    display: flex;
    justify-content: center;
    flex-grow: 1;
    cursor: pointer;
  }

  .focused {
    ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    background-color: blue;
    color: white;
    height: 100%;
    display: flex;
    align-items: center;
    transition: 0.5s;

  }

  & div.desc {
    text-align: center;
  }
`;

const Desc = styled.div`
  text-align: center;
`;

export const Tab = () => {
  // TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
  // currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.

  const menuArr = [
    { name: 'Tab1', content: 'Tab menu ONE' },
    { name: 'Tab2', content: 'Tab menu TWO' },
    { name: 'Tab3', content: 'Tab menu THREE' },
  ];
  const [currentTab, setCurrentTab] = useState(0)

  const selectMenuHandler = (index) => {
    // TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
    console.log(index)
    setCurrentTab(index);


  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}

          {menuArr.map((item, key) => {
            return <li key={key}
              className={`${key === currentTab ? 'submenu focused' : 'submenu'}`}
              onClick={() => selectMenuHandler(key)}>{item.name}</li>
          })}

        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

Tag

import { useState } from 'react';
import styled from 'styled-components';

// TODO: Styled-Component 라이브러리를 활용해 여러분만의 tag 를 자유롭게 꾸며 보세요!

export const TagsInput = styled.div`
  margin: 8rem auto;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-height: 48px;
  width: 480px;
  padding: 0 8px;
  border: 1px solid rgb(214, 216, 218);
  border-radius: 6px;

  > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;

    > .tag {
      width: auto;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      background: #4000c7;
        > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        color: #4000c7;
        border-radius: 50%;
        background: #fff;
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
    }
  }

  &:focus-within {
    border: 1px solid #4000c7;
  }
`;

export const Tag = () => {
  const initialTags = ['CodeStates', 'kimcoding'];

  const [tags, setTags] = useState(initialTags);


  const removeTags = (indexToRemove) => {
    // TODO : 태그를 삭제하는 메소드를 완성하세요.
    setTags(tags.filter(tag => {
      return tag !== tags[indexToRemove]
    }))
  };

  const addTags = (e) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    const value = e.target.value.trim();
    if (e.key === 'Enter' && !tags.includes(value) && value) {
      setTags([...tags, value])
      e.target.value = ''


    } else if (e.key === 'Enter' && !value) {
      e.target.value = ''

    }
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기


  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {tags.map((tag, index) => (
            <li key={index} className="tag">
              <span className="tag-title">{tag}</span>
              <span className="tag-close-icon"
                onClick={() => removeTags(index)}>&times;
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(e) => addTags(e)}
          // { /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */ }
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

AutoCompite

import { useState, useEffect } from 'react';
import styled from 'styled-components';

const deselectedOptions = [
  'rustic',
  'antique',
  'vinyl',
  'vintage',
  'refurbished',
  '신품',
  '빈티지',
  '중고A급',
  '중고B급',
  '골동품'
];

/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';

export const InputContainer = styled.div`
  margin-top: 8rem;
  background-color: #ffffff;
  display: flex;
  flex-direction: row;
  padding: 1rem;
  border: 1px solid rgb(223, 225, 229);
  border-radius: ${activeBorderRadius};
  z-index: 3;
  box-shadow: 0;

  &:focus-within {
    box-shadow: ${boxShadow};
  }

  > input {
    flex: 1 0 0;
    background-color: transparent;
    border: none;
    margin: 0;
    padding: 0;
    outline: none;
    font-size: 16px;
  }

  > div.delete-button {
    cursor: pointer;
  }
`;

export const DropDownContainer = styled.ul`
  background-color: #ffffff;
  display: block;
  margin-left: auto;
  margin-right: auto;
  list-style-type: none;
  margin-block-start: 0;
  margin-block-end: 0;
  margin-inline-start: 0px;
  margin-inline-end: 0px;
  padding-inline-start: 0px;
  margin-top: -1px;
  padding: 0.5rem 0;
  border: 1px solid rgb(223, 225, 229);
  border-radius: 0 0 1rem 1rem;
  box-shadow: ${boxShadow};
  z-index: 3;

  > li:hover {
    background-color: lightgray;
    } 

  > li {
    padding: 0 1rem;
    &.selected {
      background-color: lightgray;
    }
  }
`;

export const Autocomplete = () => {
  /**
    ** Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
    **- hasText state는 input값의 유무를 확인할 수 있습니다.
    ** - inputValue state는 input값의 상태를 확인할 수 있습니다.
    ** - options state는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
    */
  const [hasText, setHasText] = useState(false);
  //^ text가 들어오는 input창에 값이 들어왔는지 아닌지, 값의 유무를 갖는 상태
  const [inputValue, setInputValue] = useState('');
  //^  input창에 들어온 값을 갖는 상태

  const [options, setOptions] = useState(deselectedOptions);
  //^ input 값을 포함하는 자동완성 리스트 
  // *useEffect를 아래와 같이 활용할 수도 있습니다.
  const [selected, setSelected] = useState(-1);
  //^키보드로 option 선택할때 필요한 selected상태

  useEffect(() => {
    if (inputValue === '') { //^ 초기값
      setHasText(false); //^ input false 값 없음
      setOptions([]) //^ 값이 없으니 아래 리스트가 안나오게
    } else {
      if (inputValue !== '') {
        setOptions(deselectedOptions.filter(e => {
          return e.includes(inputValue)
        })) //^ 입력된 값을 포함하는 option들만 선택
        setHasText(true); //^ input true 값 있음 
      }
    }
  }, [inputValue]);

  // TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
  /**
   ** handleInputChange 함수는
   ** - input값 변경 시 발생되는 change 이벤트 핸들러입니다.
   **- input값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
   ** - autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보여질 수 있도록 상태를 변경합니다.
   **
   * handleInputChange 함수를 완성하여 아래 3가지 기능을 구현합니다.
   **
   * onChange 이벤트 발생 시
   ** 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
   ** 2. input값 유무 상태인 hasText가 적절하게 변경되어야 합니다.
   ** 3. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
   ** Tip : options의 상태에 따라 dropdown으로 보여지는 항목이 달라집니다.
   */
  const handleInputChange = (e) => { //^ input값 변경될때 
    setInputValue(e.target.value); //^ set으로 값 변경해줌.
    setHasText(true) //^ input값이 있으므로 true
  }
  /**
    ** handleDropDownClick 함수는
    ** - autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
    ** - dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
    **
    ** handleInputChange 함수를 완성하여 아래 기능을 구현합니다.
    **
    ** onClick 이벤트 발생 시
    ** 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
    ** 2. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
    */
  const handleDropDownClick = (clickedOption) => { //^ dropdown에서 사용하면 될거같음 
    setInputValue(clickedOption) //^ 입력값을 inputValue 변경 
  };
  /**
   ** handleDeleteButtonClick 함수는
   ** - input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
   ** - 함수 작성을 완료하여 input값을 한 번에 삭제하는 기능을 구현합니다.
   **
   ** handleDeleteButtonClick 함수를 완성하여 아래 기능을 구현합니다.
   **
   *** onClick 이벤트 발생 시
   ** 1. input값 상태인 inputValue가 빈 문자열이 되어야 합니다.
   */
  const handleDeleteButtonClick = () => {
    setInputValue('') //^ 버튼 누르면 지우기

  };

  //* Advanced Challenge: 상하 화살표 키 입력 시 dropdown 항목을 선택하고, 
  // Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
  //* 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.


  //& SSG
  const handleKeyUp = (event) => {
    //& option을 키보드로 선택할 수 있게해주는 핸들러 함수
    if (hasText) {
      //& input에 값이 있을때
      if (event.key === 'ArrowDown' && options.length - 1 > selected) {
        setSelected(selected + 1);
      }
      //& options.length에 -1을 해주는 이유는 selected의 최대값을 맞춰주기 위해서이다.
      //& 예를들어 밑에 option이 2개가 나왔다고 가정했을 때, selected값이 최대 1까지 변할 수 있게 해줘야한다. 
      //& 'ArrowDown'키를 누르면 selected는 0이 되고, 한번 더 누르면 1이 되고, 그 다음은 더이상 옵션이 없기 때문에 키가 안먹히게 해주는 것이다.

      if (event.key === 'ArrowUp' && selected >= 0) {
        //& 처음 조건을 이해했다면 여기는 자연스럽게 이해될 것이다.
        setSelected(selected - 1);
      }
      if (event.key === 'Enter' && selected >= 0) {
        //& Enter키로 option 선택
        handleDropDownClick(options[selected])
        setSelected(-1);
        //& Enter키를 눌러서 선택이 되면 다시 selected는 -1이 되야한다.
      }
    }
  }
  return (
    <div className='autocomplete-wrapper'>

      {/* TODO : input 엘리먼트를 작성하고 input값(value)을 state와 연결합니다. 
      handleInputChange 함수와 input값 변경 시 호출될 수 있게 연결합니다. */}
      {/* TODO : 아래 div.delete-button 버튼을 누르면 input 값이 삭제되어 dropdown이 없어지는 handler 함수를 작성합니다. */}
      <InputContainer>
        <input type='text'
          value={inputValue}
          defaultValue={inputValue}
          onChange={handleInputChange}
          onKeyUp={handleKeyUp}
        >
        </input>
        <div className='delete-button' onClick={handleDeleteButtonClick}>&times;</div>
      </InputContainer>
      {/* TODO : input 값이 없으면 dropdown이 보이지 않아야 합니다. 조건부 렌더링을 이용해서 구현하세요. */}
      {hasText && <DropDown options={options} handleComboBox={handleDropDownClick} selected={selected} />}
    </div>
  );
};

export const DropDown = ({ options, handleComboBox, selected }) => {
  return (
    <DropDownContainer>
      {/* TODO : input 값에 맞는 autocomplete 선택 옵션이 보여지는 역할을 합니다. */}
      {options.map((item, key) => {
        return (<li
          key={key}
          onClick={() => handleComboBox(item)}
          className={selected === key ? 'selected' : ''}
        >{item}</li>
        )
      })}
    </DropDownContainer>
  );
};

ClickToEdit

import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

export const InputBox = styled.div`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
  border: 1px #bbb dashed;
  border-radius: 10px;
  margin-left: 1rem;
`;

export const InputEdit = styled.input`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
`;

export const InputView = styled.div`
  text-align: center;
  align-items: center;
  margin-top: 3rem;

  div.view {
    margin-top: 3rem;
  }
`;


//^ MyInput 컴포넌트는 ClickToEdit 컴포넌트의 자식 컴포넌트 
//^ value 를 전달 받으며 { value } 에는 { name, age } name의 상태값과 age의 상태값
export const MyInput = ({ value, handleValueChange }) => {
  const inputEl = useRef(null);
  const [isEditMode, setEditMode] = useState(false); //^ edit 모드 상태
  const [newValue, setNewValue] = useState(value);  //^ 출력값 상태

  useEffect(() => {
    if (isEditMode) {   //^ edit 모드가 true면 input창에 포커스가 생겨서 수정이 가능하도록 설정됨.
      inputEl.current.focus();
    }
  }, [isEditMode]);

  useEffect(() => {
    setNewValue(value);
  }, [value]);

  const handleClick = () => { //^ span 태그를 클릭하면 edit모드가 활성화 useEffect에 의해 포커싱 
    // TODO : isEditMode 상태를 변경합니다.

    setEditMode(true)

  };

  const handleBlur = () => { //^ input 창이 아닌 다른 곳을 클릭하면 edit 모드를 비활성화 시킴
    // TODO : Edit가 불가능한 상태로 변경합니다.
    setEditMode(false)
    handleValueChange(newValue); //^ input 창의 입력값을 newValue로 바꿔줌 
  };

  const handleInputChange = (e) => {   // TODO : 저장된 value를 업데이트합니다.
    setNewValue(e.target.value)  //^ input에 입력한 값을  newValue에 할당

  };

  return (
    <InputBox>
      {/*//^ edit 모드가 활성화 되면 input 태그 , 비활성화 되면 span태그  */}
      {isEditMode ? (
        // TODO : 포커스를 잃으면 Edit가 불가능한 상태로 변경되는 메소드가 실행되어야 합니다.
        // TODO : 변경 사항이 감지되면 저장된 value를 업데이트 되는 메소드가 실행되어야 합니다.
        <InputEdit
          type='text'
          value={newValue}
          ref={inputEl}
          onBlur={handleBlur}
          onChange={handleInputChange}
        />
      ) : (
        // TODO : 클릭하면 Edit가 가능한 상태로 변경되어야 합니다.
        <span
          onClick={handleClick}
        >{newValue}</span>
      )}
    </InputBox>
  );
}

const cache = {
  name: '김코딩',
  age: 20
};

export const ClickToEdit = () => {
  const [name, setName] = useState(cache.name);
  const [age, setAge] = useState(cache.age);

  return (
    <>
      <InputView>
        <label>이름</label>
        <MyInput value={name} handleValueChange={(newValue) => setName(newValue)} />
      </InputView>
      <InputView>
        <label>나이</label>
        <MyInput value={age} handleValueChange={(newValue) => setAge(newValue)} />
      </InputView>
      <InputView>
        <div className='view'>이름 {name} 나이 {age}</div>
      </InputView>
    </>
  );
};
profile
개발자가 되고싶은 잡초

0개의 댓글