# Today I Learn - 2022.05.10

# Pre Onboarding Course

// 1
// onClick?: (e: MouseEvent<HTMLButtonElement>) => void
// setChecked?: (e: ChangeEvent<HTMLInputElement>) => void
// onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
// postSubmit: (e: FormEvent<HTMLFormElement>) => void

// 2
interface ButtonProps extends Omit<ButtonHTMLAttributes<unknown>, 'translate'> {
  children: string | JSX.Element | JSX.Element[]
  className?: string
  disabled?: boolean
  isSubmit?: boolean
  loading?: boolean
  name?: string
  onClick?: (e: MouseEvent<HTMLButtonElement>) => void
  btnRef?: RefObject<HTMLButtonElement>
  size: 'unset' | 'big' | 'medium' | 'small' | 'smallest'
  shape: 'unset' | 'reward' | 'rounded' | 'primary' | 'order' | 'ghost' | 'ghostLight' | 'line'
  translate?: boolean
  url?: string
  inline?: boolean
  danger?: boolean // ghost 빨간 버전
}

interface ButtonProps {
  text: string
  type:
    | 'textWithArrow'
    | 'textWithCopied'
    | 'textWithIcon'
    | 'copyButtonOnly'
    | 'iconOnly'
    | 'textOnly'
    | 'textToCopied'
  handleCopy?: () => void
}

interface Props {
  className?: string
  children: ReactNode
  asFooter?: boolean
}

// 3
interface Props {
  transactionUrl: string
  currency: string
  txId: string
  transferState: ETransferState
  arrow?: boolean
  inline?: boolean
  shape: 'line' | 'rounded'
}

export const ButtonTXID = ({
  transactionUrl,
  currency,
  txId,
  transferState,
  arrow,
  inline,
  shape,
}: Props): JSX.Element | null => {
  if (!currency || !txId) return null
  if (!isTxidAvailable(transferState)) return null

  return (
    <Button url={transactionUrl} className={styles.txidButton} shape={shape} size='unset' inline={inline}>
      <span>TX ID</span>
      {arrow ? <ArrowIcon /> : <></>}
    </Button>
  )
}

// 4
import styles, { cx } from 'styles'

import { useI18n } from 'hooks'
import Lottie from 'utils/lottie'

import LoadingAni from '@/assets/animations/loading.json'
import LoadingAniDark from '@/assets/animations/loading-dark.json'

interface Props {
  size: 'tiny' | 'small' | 'big' | 'bigger' | 'large' | 'huge'
  withLabel?: boolean
  className?: string
  forceDark?: boolean
  forceBright?: boolean
  full?: boolean
}

export const LoadingSpinner = (props: Props): JSX.Element => {
  const { size, withLabel, className, forceDark, forceBright, full } = props
  const t = useI18n()
  const wrapperClassName = cx(styles.loadingSpinner, className, styles[size], {
    [styles.forceDark]: forceDark,
    [styles.forceBright]: forceBright,
    [styles.full]: full,
  })

  return (
    <div className={wrapperClassName}>
      <Lottie animationData={LoadingAni} className={styles.brightSpinner} />
      <Lottie animationData={LoadingAniDark} className={styles.darkSpinner} />
      {withLabel && <p>{t('front:global.loading')}</p>}
    </div>
  )
}

// 5
function getActiveWidth(
  percentage: number,
  isDesktop: boolean,
  mobileMaxWidth: number,
  atTrade?: boolean,
  atWallet?: boolean): number {
  if (atTrade) {
    const maxWidth = isDesktop ? 295 : mobileMaxWidth
    const dirtyWidth = maxWidth * percentage
    const cleanWidth = dirtyWidth - (dirtyWidth % 5) // 5의 배수로 고정
    return Math.max(cleanWidth, 5)
  }

  if (atWallet) {
    const maxWidth = isDesktop ? 480 : mobileMaxWidth
    const dirtyWidth = maxWidth * percentage
    const cleanWidth = dirtyWidth - (dirtyWidth % 8) // 8의 배수로 고정
    return Math.max(cleanWidth, 8)
  }

  return 0
}

// 6
import { Dispatch, MouseEvent, SetStateAction } from 'react'
import styles, { cx } from 'styles'

import { useI18n } from 'hooks'
import { LoadingSpinner } from '@/components/_common/LoadingSpinner'

export interface SwitchItem {
  name: string
  label?: string
  icon?: JSX.Element
  disabled?: boolean
}

interface Props<T> {
  items: SwitchItem[]
  mode: T
  setMode: (newMode: T) => void | Dispatch<SetStateAction<T>>
  className?: string
  type: 'dTag' | 'default'
  color: 'common' | 'trade' | 'setting'
  isLoading?: boolean
}

export const CommonSwitch = <T extends string>({
  items,
  mode,
  setMode,
  className,
  type,
  color,
  isLoading,
}: Props<T>): JSX.Element => {
  const t = useI18n()

  const handleMode = (e: MouseEvent<HTMLButtonElement>): void => {
    const { name } = e.currentTarget
    setMode(name as T)
  }

  const wrapperClassName = cx(styles.commonSwitch, styles[type], styles[color], className, {
    [styles.right]: mode === items[1].name,
  })

  if (isLoading) {
    return (
      <div className={wrapperClassName}>
        <LoadingSpinner size='small' forceDark />
      </div>
    )
  }

  return (
    <div className={wrapperClassName}>
      {items.map((item) => {
        return (
          <button
            key={commonSwitch-${item.name}}
            type='button'
            onClick={handleMode}
            name={item.name}
            disabled={item.disabled}
            className={cx({ [styles.active]: mode === item.name })}
            tabIndex={-1}
          >
            {item.icon || t(item.label || '')}
          </button>
        )
      })}
      <div className={styles.aniBg} />
    </div>
  )
}

// 7
import { MouseEvent } from 'react'

import DatePicker from 'react-datepicker'
import styles from 'styles'

import { SetterOrUpdater } from 'stateHooks'
import { useClickAway, useGA, useI18n, useRef, useState } from 'hooks'
import { TDateFilter } from 'types/histories.d'

import { Button } from '../_common/Button'
import DatePickerHeader from './DatePickerHeader'
import { CommonButtons } from '@/components/_common/Buttons'

interface Props {
  date: TDateFilter
  setDate: SetterOrUpdater<TDateFilter> | ((value: Date | null) => void)
  handleClose: () => void
  gaAction: string
}

const CustomDatePicker = ({ date, setDate, handleClose: handleClose2, gaAction }: Props): JSX.Element => {
  const datePickerRef = useRef<HTMLDivElement>(null)

  const { gaEvent } = useGA()
  const t = useI18n()

  const [selected, setSelected] = useState(date)

  useClickAway(datePickerRef, handleClose2)

  const renderDayContents = (day: number): JSX.Element => {
    return (
      <div>
        <span className={styles.dayContent}>{day}</span>
      </div>
    )
  }

  const handleChange = (d: Date): void => setSelected(d)

  const handleConfirm = (): void => {
    setDate(selected)
    handleClose2()
    gaEvent({ action: `${gaAction}_calendar_search` })
  }

  const handleClose = (e: MouseEvent<HTMLButtonElement>) => {
    return e
  }

// 8
  return (
    <div className={styles.datePicker} ref={datePickerRef} translate='no'>
      <DatePicker
        selected={selected}
        onChange={handleChange}
        inline
        renderDayContents={renderDayContents}
        maxDate={new Date()}
        showMonthDropdown
        useShortMonthInDropdown
        renderCustomHeader={({ date: d, changeYear, changeMonth, decreaseMonth, increaseMonth }): JSX.Element => (
          <DatePickerHeader
            date={d}
            changeYear={changeYear}
            changeMonth={changeMonth}
            decreaseMonth={decreaseMonth}
            increaseMonth={increaseMonth}
          />
        )}
      />
      <CommonButtons asFooter>
        <Button shape='ghost' size='big' onClick={handleClose} translate>
          {t('front:global.cancel')}
        </Button>
        <Button shape='primary' size='big' onClick={handleConfirm} translate>
          {t('front:global.go')}
        </Button>
      </CommonButtons>
    </div>
  )
}

export default CustomDatePicker

// 9
import { ChangeEvent, Dispatch, MouseEvent, SetStateAction, useMemo } from 'react'
import styles, { cx } from 'styles'

import { useState } from 'hooks'

import { PageHandleIcon, TooltipIcon } from 'svgs'

interface SliderProps {
  currentPage: number
  totalPage: number
  tooltips?: string[]
  setPage: Dispatch<SetStateAction<number>> | ((value: number) => void)
  wrapperWidth: number
  scrollToTop: () => void
}

const Slider = ({
  currentPage,
  totalPage,
  tooltips,
  setPage,
  wrapperWidth,
  scrollToTop,
}: SliderProps): JSX.Element | null => {
  const [hover, setHover] = useState<{
    date?: string
    x?: number
  }>({})

  const sliderX = useMemo(() => currentPage - 1, [currentPage])

  const handleOnChange = (event: ChangeEvent<HTMLInputElement>): void => {
    const { value } = event.currentTarget
    setPage(Number(value) + 1)
  }

  const handleMouseOut = (): void => setHover({})

  const handleHover = (e: MouseEvent): void => {
    if (!tooltips) return

    const { x } = e.currentTarget.getBoundingClientRect()
    const hoverWidth = wrapperWidth / totalPage
    const hoveredPageNumber = parseInt(`${(e.clientX - x) / hoverWidth}`, 10)
    const date = tooltips[hoveredPageNumber]
    if (!date) return

    setHover({
      date,
      x: e.clientX - x,
    })
  }

// 10
  return (
    <div className={styles.sliderWrapper} onMouseMove={handleHover} onMouseOut={handleMouseOut} onBlur={handleMouseOut}>
      <input
        type='range'
        step={0}
        min={0}
        max={totalPage - 1}
        value={sliderX}
        onChange={handleOnChange}
        onMouseUp={scrollToTop}
      />
      <div
        className={cx(styles.fakeHandle, { [styles.hover]: hover.date })}
        style={{
          left: `${(wrapperWidth / totalPage) * sliderX}px`,
          width: `${wrapperWidth / totalPage}px``,
        }}
      >
        <PageHandleIcon />
      </div>
      {hover.date && (
        <div className={styles.hoverDate} style={{ left: hover.x }}>
          <p>{hover.date}</p>
          <TooltipIcon role='tooltip' />
        </div>
      )}
      <div className={styles.fakeTrack} />
    </div>
  )
}

export default Slider

// 11
export interface WalletDataCore {
  flatFee: string
  rateFee: string
  minAmount: string
  maxAmount: string
  validFrom: number
}

export interface FeeListItem extends WalletDataCore {
  blockchainName: string
  currency: string
}

export interface FeeListData {
  withdrawalFees: FeeListItem[]
}

export interface WithdrawBalanceData extends WalletDataCore {
  withdrawableAmount24h: string
}

export interface AddressData {
  address: string
  addressParams?: {
    destinationTag: string
  }
}

export interface WithdrawalParams {
  blockchainName?: string
  currency: string
  address: string
  addressParams?: {
    destinationTag: string
  }
  netAmount: string
  fee: string
  token: string
}

export interface WithdrawalSucessData {
  id: string
}

export interface WithdrawalErrorData {
  error: string
  errorDetails: {
    reason: string
    expiredAt: number
  }
}

// 12
/* eslint-disable camelcase */

export interface ZendeskArticle {
  id: number
  html_url: string
  created_at: string
  section_id: number
  title: string
  body: string
}

export interface ZendeskArticles {
  articles: ZendeskArticle[]
  count: number
  next_page: boolean
  page: number
  page_count: number
  per_page: number
  previous_page: boolean
  sort_by: string
  sort_order: string
}
  1. eslint & prettier의 중요성 : eslint & prettier 항상 적용해야한다. error나 warning이 절대 보이지 않게 해달라고 다시 한번 강요하셨다.
  2. README 작성 : README는 확인하는 사람이 폴더 구조를 생각할 수 있게 해주도록 프로젝트의 설계를 파악할 수 있게끔 작성한다.
  3. scss module 파일명 : example.module.scss와 같이 scss의 경우 파일명은 소문자로 작성하는 것이 좋다.
  4. 공통 컴포넌트 : components 폴더 내부에 공통으로 쓰는 경우 컴포넌트는 common 폴더를 하나 만들어서 그 내부에 사용할 수도 있다. 이렇게 공통으로 쓰는 컴포넌트를 별도로 관리하여 다른 프로젝트 진행 시 common 폴더 자체를 복붙해서 템플릿처럼 사용할 수 있다.
  5. children과 outlet : 최근 react에서는 children을 대체하여 outlet을 사용하기도 한다.
  6. suspense : use suspense를 사용하여 spinner 구현 코드를 깔끔하게 작성할 수 있다. (참고 : https://ko.reactjs.org/docs/concurrent-mode-suspense.html)
  7. api의 instance화 : api는 src -> lib -> services 폴더 내부에 인스턴스로 만들어 사용한다.
  8. axios vs ky : axios는 ky와 다르게 time out을 잘 잡지 못한다.
  9. 디바운스와 스로틀링 : 디바운스(Debounce)와 스로틀(Throttle)를 활용한다. 수영님이 추천해주신 디바운스 시간은 300을 추천하셨고 원래는 서버개발자랑 맞추어가는 것이 좋다고 한다. (참고 : https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa)
  10. import, state 정리 : import할 때나 state부분에 용도 혹은 어디서 가져오냐를 구분하여 정리해두면 가독성이 좋아진다. import의 경우 예시로 라이브러리 -> 훅 or 타입 -> svg의 순서로 나누어 정리한다.
  11. 중첩 구조분해할당 : 구조분해할당 사용 시 두번의 구조분해가 필요한 경우 한번에 해도 가독성이 괜찮다면 중첩하여 한번에 구조분해하여 코드의 길이를 줄이고 가독성을 좋게한다.
  12. ts에서 선언식과 표현식 : 타입스크립트에서 함수 선언식이나 함수 표현식이나 타입추론이 달랐던 차이가 있었으나 버전이 업그레이드되면서 무관해졌다. (ex const Component (() => {}) / function Component() {})
  13. state의 배열 : state의 값이 동적으로 들어가며 그 값이 array인 경우 초기값을 넣지 않는 것보다 빈 array로 넣어주는 것이 좋다.