Don't be afraid of challenges

[nextjs] contextAPI로 alert, modal창 만들기 본문

nextjs

[nextjs] contextAPI로 alert, modal창 만들기

초아롱 2024. 7. 10. 21:04

1. alert

import { ToastType } from '@/types/toast.type';
import { cva } from 'class-variance-authority';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import CloseIcon from '../../../public/icons/ic-close.png';
import InfoIcon from '../../../public/icons/ic-info.png';
interface AlertProps {
  toast: ToastType;
}

const alertVariants = cva(
  'bg-gray-200 border border-l-4 border-l-gray-500 w-[300px] translation rounded-md px-4 py-3 shadow-sm flex flex-row justify-between items-center duration-500',
  {
    variants: {
      isDisplayed: {
        true: 'translate-y-[calc(20px)]',
        false: 'translate-y-[calc(-100%)]'
      }
    },
    defaultVariants: {
      isDisplayed: false
    }
  }
);
function Alert({ toast }: AlertProps) {
  const [isDisplayed, setIsDisplayed] = useState<boolean>(false);

  useEffect(() => {
    setIsDisplayed(true);
    setTimeout(() => setIsDisplayed(false), 2000 - 500);
  }, []);

  const handleDelete = () => {
    setIsDisplayed(false);
  };
  return (
    <div className={alertVariants({ isDisplayed })}>
      <div className="flex flex-row gap-3">
        <Image src={InfoIcon} width={25} height={25} alt={'info icon'} />
        <span>{toast.label}</span>
      </div>

      <div>
        <Image
          src={CloseIcon}
          width={15}
          height={15}
          alt={'close button'}
          className="hover:cursor-pointer"
          onClick={handleDelete}
        />
      </div>
    </div>
  );
}

export default Alert;

 

'use client';

import Alert from '@/components/Alert';
import { ToastProps, ToastType } from '@/types/toast.type';
import { createContext, PropsWithChildren, useContext, useState } from 'react';

const initialValue: ToastProps = {
  on: () => {},
  off: () => {}
};

export const ToastContext = createContext(initialValue);

export const useToast = () => useContext(ToastContext);

export function ToastProvider({ children }: PropsWithChildren) {
  const [toasts, setToasts] = useState<ToastType[]>([]);
  const value: ToastProps = {
    on: (toast) => {
      const id = crypto.randomUUID();
      if (toasts.some((t) => t.label === toast.label)) return;
      setToasts((prev) => [...prev, { ...toast, id }]);

      setTimeout(() => {
        setToasts((prev) => prev.filter((toast) => toast.id !== id));
      }, 2000);
    },
    off: (id) => {
      setToasts((prev) => prev.filter((toast) => toast.id !== id));
    }
  };
  return (
    <ToastContext.Provider value={value}>
      {toasts.length > 0 && (
        <ul className="fixed right-6 z-20 grid grid-cols-1 gap-y-3">
          {toasts.map((toast: ToastType) => (
            <li key={toast.id}>
              <Alert toast={toast} />
            </li>
          ))}
        </ul>
      )}
      {children}
    </ToastContext.Provider>
  );
}

 

import Page from '@/components/Page';
import PokemonList from '@/components/PokemonList';
import SearchBar from '@/components/SearchBar';
import { ConfirmProvider } from '@/contexts/confirm.context';
import { ToastProvider } from '@/contexts/toast.context';

export default function HomePage() {
  return (
    <ToastProvider>
      <ConfirmProvider>
        <Page title="포켓몬 도감">
          <SearchBar />
          <PokemonList />
        </Page>
      </ConfirmProvider>
    </ToastProvider>
  );
}

 

 

2.모달

'use client';
import { useConfirm } from '@/contexts/confirm.context';
import { ModalType } from '@/types/toast.type';
import Image from 'next/image';
import InfoIcon from '../../../public/icons/ic-info.png';
import BackDrop from '../BackDrop';
interface ModalProps {
  modalOptions: ModalType | null;
  handleClick: () => void;
}
function ConfirmModal({ modalOptions, handleClick }: ModalProps) {
  const modal = useConfirm();
  return (
    <BackDrop>
      <div className="bg-white border rounded-md p-4 w-[320px] shadow-sm" onClick={(e) => e.stopPropagation()}>
        <div className="flex flex-row gap-2 justify-center">
          <Image src={InfoIcon} width={25} height={25} alt={'info icon'} />
          <span>시스템알림</span>
        </div>
        <h1 className="font-semibold text-xl my-8 text-center">{modalOptions!.label}</h1>

        <div className="flex flex-row gap-3 justify-between">
          <button onClick={handleClick}>확인</button>
          <button onClick={() => modal.off()}>취소</button>
        </div>
      </div>
    </BackDrop>
  );
}

export default ConfirmModal;

 

'use client';
import { ModalProps } from '@/types/toast.type';
import { createContext, PropsWithChildren, useContext, useState } from 'react';

const initialValue: ModalProps = {
  modalOptions: null,
  on: () => {},
  off: () => {}
};

export const ConfirmContext = createContext(initialValue);

export const useConfirm = () => useContext(ConfirmContext);

export function ConfirmProvider({ children }: PropsWithChildren) {
  const [modalOptions, setModalOptions] = useState<ModalProps['modalOptions']>(null);
  const value: ModalProps = {
    modalOptions,
    on: (toast) => {
      setModalOptions(toast);
    },
    off: () => {
      setModalOptions(null);
    }
  };
  return <ConfirmContext.Provider value={value}>{children}</ConfirmContext.Provider>;
}

 

 

'nextjs' 카테고리의 다른 글

쿼리 파라미터를 이용한 검색로직 구현  (0) 2025.01.12
[nextjs] 페이지네이션  (1) 2024.07.11
[nextjs] 좋아요 기능 구현 + 낙관적 업데이트  (0) 2024.07.10
[nextjs] Rendering  (0) 2024.07.03
[nextjs] routing  (1) 2024.07.03