Skip to content

Commit

Permalink
Merge pull request #101 from softeerbootcamp4th/feat/#98-admin-component
Browse files Browse the repository at this point in the history
[Feat] 어드민 공통 컴포넌트 구현
  • Loading branch information
jhj2713 authored Aug 7, 2024
2 parents 3c7bf6b + bf5133b commit 15c862a
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 22 deletions.
3 changes: 3 additions & 0 deletions admin/public/assets/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions admin/src/components/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { HTMLProps } from "react";
import { cva } from "class-variance-authority";

interface ButtonProps extends HTMLProps<HTMLButtonElement> {
isValid?: boolean;
type: "lg" | "sm";
}

const ButtonVariants = cva(`transition-all`, {
variants: {
isValid: {
true: "text-neutral-950 border-neutral-950 bg-white hover:bg-neutral-100",
false: "text-neutral-300 border-neutral-300 bg-neutral-100",
},
type: {
lg: "w-[266px] rounded-full py-[16px] border-2",
sm: "inline px-[12px] py-[8px] rounded-xl border",
},
},
});

export default function Button({ isValid = true, type, children, ...restProps }: ButtonProps) {
return (
<button className={ButtonVariants({ isValid, type })} disabled={!isValid} {...restProps}>
{children}
</button>
);
}
45 changes: 45 additions & 0 deletions admin/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useState } from "react";

interface DropdownProps {
options: string[];
selectedIdx: number;
handleClickOption: (idx: number) => void;
}

export default function Dropdown({ options, selectedIdx, handleClickOption }: DropdownProps) {
const [isVisibleOptions, setIsVisibleOptions] = useState<boolean>(false);

const handleClick = (idx: number) => {
handleClickOption(idx);
setIsVisibleOptions(false);
};

return (
<>
<div
className="fixed w-screen h-screen left-0 top-0"
onClick={() => setIsVisibleOptions(false)}
/>
<div className="relative inline-block text-left">
<div onClick={() => setIsVisibleOptions(!isVisibleOptions)}>
{options[selectedIdx]}
</div>
{isVisibleOptions && (
<div className="origin-top-right absolute right-0 mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="p-[16px] flex flex-col gap-2">
{options.map((option, idx) => (
<p
key={`dropdown-${option}-${idx}`}
onClick={() => handleClick(idx)}
className="break-keep text-nowrap"
>
{option}
</p>
))}
</div>
</div>
)}
</div>
</>
);
}
7 changes: 7 additions & 0 deletions admin/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Header() {
return (
<header className="w-full h-[80px] bg-neutral-700 text-neutral-200 h-body-1-bold px-10 flex items-center">
현대자동차 캐스퍼 일렉트릭 신차 출시 이벤트 어드민
</header>
);
}
19 changes: 19 additions & 0 deletions admin/src/components/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HTMLProps } from "react";

interface InputProps extends HTMLProps<HTMLInputElement> {
label?: string;
}

export default function Input({ label, value, onChange, ...restProps }: InputProps) {
return (
<div className="flex items-center">
{label && <p className="text-neutral-950 h-body-1-bold mx-[20px]">{label}</p>}
<input
className="p-[16px] border border-neutral-950 rounded-lg text-neutral-950 w-[360px] h-body-1-medium"
value={value}
onChange={onChange}
{...restProps}
/>
</div>
);
}
27 changes: 27 additions & 0 deletions admin/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PropsWithChildren } from "react";

export interface ModalProps extends PropsWithChildren {
handleClose: () => void;
}

export default function Modal({ handleClose, children }: ModalProps) {
return (
<div className="fixed w-full h-full left-0 top-0 z-20">
<div
className="absolute left-0 top-0 w-[100%] h-[100%] bg-black/[.4]"
onClick={handleClose}
/>
<div className="absolute left-[50%] top-[50%] translate-y-[-50%] translate-x-[-50%] bg-white p-[80px] rounded-3xl">
{children}

<button onClick={handleClose}>
<img
className="absolute right-[32px] top-[32px]"
alt="모달 닫기 버튼"
src="/assets/icons/close.svg"
/>
</button>
</div>
</div>
);
}
32 changes: 32 additions & 0 deletions admin/src/components/TabHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cva } from "class-variance-authority";

interface TabHeaderProps {
tabList: string[];
handleClickTab: (idx: number) => void;
selectedIdx: number;
}

const TabButtonVariants = cva(`border-b-2`, {
variants: {
selected: {
true: "h-body-1-bold border-neutral-950",
false: "h-body-1-regular border-transparent",
},
},
});

export default function TabHeader({ tabList, selectedIdx, handleClickTab }: TabHeaderProps) {
return (
<div className="w-full h-[80px] flex px-[40px] gap-[40px]">
{tabList.map((tab, idx) => (
<button
key={idx}
className={TabButtonVariants({ selected: selectedIdx === idx })}
onClick={() => handleClickTab(idx)}
>
{tab}
</button>
))}
</div>
);
}
43 changes: 43 additions & 0 deletions admin/src/components/Table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactNode } from "react";

interface TableProps {
headers: ReactNode[];
data: ReactNode[][];
}

export default function Table({ headers, data }: TableProps) {
return (
<div className="relative sm:rounded-lg w-[1560px] h-[600px] border">
<div className="overflow-y-auto h-full">
<table className="w-full text-sm rtl:text-right text-gray-500 dark:text-gray-400 text-center">
<thead className="sticky top-0 z-10 text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
{headers.map((header, idx) => (
<th key={idx} scope="col" className="px-6 py-3 h-body-2-medium">
{header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((tableData, idx) => (
<tr
key={`table-data-${idx}`}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
>
{tableData.map((dataNode, idx) => (
<td
key={`${headers[idx]}-data-${idx}`}
className="px-6 py-4 h-body-2-regular"
>
{dataNode}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions admin/src/hooks/useModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
import Modal, { ModalProps } from "@/components/Modal";

export default function useModal() {
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
return () => {
document.body.style.overflow = "unset";
};
}, []);

const handleOpenModal = () => {
document.body.style.overflow = "hidden";
setIsOpen(true);
};

const handleCloseModal = () => {
document.body.style.overflow = "unset";
setIsOpen(false);
};

const ModalComponent = ({ children }: Omit<ModalProps, "handleClose">) => {
return isOpen ? <Modal handleClose={handleCloseModal}>{children}</Modal> : null;
};

return { handleOpenModal, ModalComponent };
}
39 changes: 18 additions & 21 deletions admin/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,41 @@
@tailwind components;
@tailwind utilities;

input:focus {
outline: none;
}

@font-face {
font-family: "HyundaiSansHeadOffice-Bold";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFBold.otf")
format("opentype");
font-family: "HyundaiSansHeadOffice-Bold";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFBold.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansHeadOffice-Medium";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFMedium.otf")
format("opentype");
font-family: "HyundaiSansHeadOffice-Medium";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFMedium.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansHeadOffice-Regular";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFRegular.otf")
format("opentype");
font-family: "HyundaiSansHeadOffice-Regular";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFRegular.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansHeadOffice-Light";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFLight.otf")
format("opentype");
font-family: "HyundaiSansHeadOffice-Light";
src: url("/public/fonts/hyundai-sans/HyundaiSansHeadKROTFLight.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansTextOffice-Bold";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFBold.otf")
format("opentype");
font-family: "HyundaiSansTextOffice-Bold";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFBold.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansTextOffice-Medium";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFMedium.otf")
format("opentype");
font-family: "HyundaiSansTextOffice-Medium";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFMedium.otf") format("opentype");
}

@font-face {
font-family: "HyundaiSansTextOffice-Regular";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFRegular.otf")
format("opentype");
font-family: "HyundaiSansTextOffice-Regular";
src: url("/public/fonts/hyundai-sans/HyundaiSansTextKROTFRegular.otf") format("opentype");
}
87 changes: 87 additions & 0 deletions admin/src/pages/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ChangeEvent, useState } from "react";
import Button from "@/components/Button";
import Dropdown from "@/components/Dropdown";
import Header from "@/components/Header";
import Input from "@/components/Input";
import TabHeader from "@/components/TabHeader";
import Table from "@/components/Table";
import useModal from "@/hooks/useModal";

export default function Login() {
const [selectedIdx, setSelectedIdx] = useState<number>(0);
const [value, setValue] = useState<string>("");

const { handleOpenModal, ModalComponent } = useModal();

const [selectedDropdownIdx, setSelectedDropdownIdx] = useState(0);

const headers = [
"ID",
"이벤트 진행 날짜",
"오픈 시간",
"종료 시간",
"활성화 시간",
"선택지 관리",
"경품 관리",
"선착순 당첨 인원 수",
"진행 상태",
<Dropdown
options={["옵션 1 : 총 선택 인원 13,245", "옵션 2 : 총 선택 인원 10,182"]}
selectedIdx={selectedDropdownIdx}
handleClickOption={(idx: number) => setSelectedDropdownIdx(idx)}
/>,
"관리",
];
const data = new Array(20).fill(null).map(() => [
7,
"2024-07-19",
"22:00:00",
"22:10:00",
"00시간 10분 00초",
<Button type="sm">선택지 관리</Button>,
<Button type="sm">경품 관리</Button>,
<div className="flex justify-between">
<p>315</p>
<p>편집</p>
</div>,
"오픈 전",
<Button type="sm">참여자 리스트 보기</Button>,
<Button type="sm">삭제</Button>,
]);

return (
<>
<Header />
<TabHeader
tabList={["캐스퍼 일렉트릭 봇 만들기 추첨 이벤트", "선착순 밸런스 게임 이벤트"]}
selectedIdx={selectedIdx}
handleClickTab={(idx) => setSelectedIdx(idx)}
/>
<Button type="lg" onClick={handleOpenModal}>
임시 저장
</Button>
<Button type="sm">임시 저장</Button>
<Button type="lg" isValid={false}>
임시 저장
</Button>
<Button type="sm" isValid={false}>
임시 저장
</Button>
<Input
value={value}
onChange={(e) => setValue((e as ChangeEvent<HTMLInputElement>).target.value)}
/>
<Input
label="ID"
value={value}
onChange={(e) => setValue((e as ChangeEvent<HTMLInputElement>).target.value)}
/>

<ModalComponent>
<div className="w-[200px] h-[128px]">hihi</div>
</ModalComponent>

<Table headers={headers} data={data} />
</>
);
}
Loading

0 comments on commit 15c862a

Please sign in to comment.