Skip to content

Commit

Permalink
Merge pull request #104 from softeerbootcamp4th/feat/#103-admin-ui
Browse files Browse the repository at this point in the history
[Feat] 어드민 UI 구현
  • Loading branch information
jhj2713 authored Aug 7, 2024
2 parents 15c862a + 125a07c commit 6d68e8b
Show file tree
Hide file tree
Showing 30 changed files with 955 additions and 284 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/admin_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main
- dev
paths:
- 'admin/**'
- "admin/**"

jobs:
build-admin:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/client_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches:
- dev
paths:
- 'client/**'
- "client/**"

jobs:
build-client:
Expand Down Expand Up @@ -58,4 +58,4 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: '빌드를 실패했습니다. :x: 자세한 내용은 로그를 참고해주세요.'
})
})
21 changes: 11 additions & 10 deletions admin/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hybrid-JGS Admin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hybrid-JGS Admin</title>
</head>
<body>
<div id="root"></div>

<script type="module" src="/src/index.tsx"></script>
</body>
</html>
9 changes: 9 additions & 0 deletions admin/public/assets/icons/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions admin/public/assets/icons/down-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions admin/public/assets/icons/left-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion admin/src/assets/react.svg

This file was deleted.

23 changes: 16 additions & 7 deletions admin/src/components/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { HTMLProps } from "react";
import { ButtonHTMLAttributes } from "react";
import { cva } from "class-variance-authority";

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

const ButtonVariants = cva(`transition-all`, {
Expand All @@ -12,16 +12,25 @@ const ButtonVariants = cva(`transition-all`, {
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",
size: {
lg: "w-[266px] rounded-full py-[16px] border",
sm: "inline px-[12px] py-[8px] rounded-xl border",
},
},
});

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

interface DatePickerProps {
date: string;
onChangeDate: (date: string) => void;
}

export default function DatePicker({ date, onChangeDate }: DatePickerProps) {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChangeDate(e.target.value);
};

return (
<div className="relative max-w-sm">
<div className="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<img alt="달력 아이콘" src="/assets/icons/calendar.svg" />
</div>
<input
type="date"
value={date}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5"
placeholder="Select date"
/>
</div>
);
}
60 changes: 34 additions & 26 deletions admin/src/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";

interface DropdownProps {
options: string[];
Expand All @@ -8,38 +8,46 @@ interface DropdownProps {

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

const handleClose = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsVisibleOptions(false);
}
};

useEffect(() => {
document.addEventListener("click", handleClose);

return () => document.removeEventListener("click", handleClose);
}, []);

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 className="relative inline-block text-left z-10 cursor-pointer" ref={dropdownRef}>
<div onClick={() => setIsVisibleOptions(!isVisibleOptions)} className="flex gap-1">
<p>{options[selectedIdx]}</p>
<img alt="드롭다운 토글 아이콘" src="/assets/icons/down-arrow.svg" />
</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>
);
}
16 changes: 16 additions & 0 deletions admin/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Outlet } from "react-router-dom";
import { RushEventContext } from "@/contexts/rushEventContext";
import Header from "../Header";

export default function Layout() {
return (
<div className="overflow-hidden h-screen">
<Header />
<div className="mb-[80px]">
<RushEventContext>
<Outlet />
</RushEventContext>
</div>
</div>
);
}
34 changes: 21 additions & 13 deletions admin/src/components/TabHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { useEffect, useState } from "react";
import { cva } from "class-variance-authority";
import { Link, useLocation } from "react-router-dom";
import { TAB_OPTIONS } from "@/constants/tabOptions";

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

const TabButtonVariants = cva(`border-b-2`, {
const TabButtonVariants = cva(`border-b-2 flex items-center`, {
variants: {
selected: {
true: "h-body-1-bold border-neutral-950",
Expand All @@ -15,17 +12,28 @@ const TabButtonVariants = cva(`border-b-2`, {
},
});

export default function TabHeader({ tabList, selectedIdx, handleClickTab }: TabHeaderProps) {
export default function TabHeader() {
const [selectedIdx, setSelectedIdx] = useState<number>(0);

const location = useLocation();

useEffect(() => {
const pathname = location.pathname;
const tabIdx = TAB_OPTIONS.findIndex((tab) => pathname.startsWith(tab.route));

setSelectedIdx(tabIdx);
}, [location]);

return (
<div className="w-full h-[80px] flex px-[40px] gap-[40px]">
{tabList.map((tab, idx) => (
<button
{TAB_OPTIONS.map((tab, idx) => (
<Link
key={idx}
className={TabButtonVariants({ selected: selectedIdx === idx })}
onClick={() => handleClickTab(idx)}
to={TAB_OPTIONS[idx].route}
>
{tab}
</button>
{tab.title}
</Link>
))}
</div>
);
Expand Down
9 changes: 3 additions & 6 deletions admin/src/components/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ 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">
<table className="w-full text-sm rtl:text-right text-gray-500 text-center">
<thead className="sticky top-0 z-[5] text-gray-700 bg-gray-50">
<tr>
{headers.map((header, idx) => (
<th key={idx} scope="col" className="px-6 py-3 h-body-2-medium">
Expand All @@ -21,10 +21,7 @@ export default function Table({ headers, data }: TableProps) {
</thead>
<tbody>
{data.map((tableData, idx) => (
<tr
key={`table-data-${idx}`}
className="bg-white border-b dark:bg-gray-800 dark:border-gray-700"
>
<tr key={`table-data-${idx}`} className="bg-white border-b">
{tableData.map((dataNode, idx) => (
<td
key={`${headers[idx]}-data-${idx}`}
Expand Down
49 changes: 49 additions & 0 deletions admin/src/components/TimePicker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ChangeEvent } from "react";

interface TimePickerProps {
time: string;
onChangeTime: (time: string) => void;
}

export default function TimePicker({ time, onChangeTime }: TimePickerProps) {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
/**
* 시간-분 까지만 선택 가능
* 초는 0초를 디폴트로 넣는다
*/
const time = `${e.target.value}:00`;
onChangeTime(time);
};

return (
<div className="max-w-[8rem] mx-auto">
<div className="relative">
<div className="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
<svg
className="w-4 h-4 text-gray-500"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fillRule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z"
clipRule="evenodd"
/>
</svg>
</div>
<input
type="time"
id="time"
className="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
min="09:00"
max="18:00"
value={time}
onChange={handleChange}
required
/>
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions admin/src/constants/rush.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const RUSH_SECTION = {
EVENT_LIST: "event_list",
APPLICANT_LIST: "applicant_list",
SELECTION_MANAGE: "selection_manage",
GIFT_MANAGE: "gift_manage",
};
export type RushSectionType = (typeof RUSH_SECTION)[keyof typeof RUSH_SECTION];
4 changes: 4 additions & 0 deletions admin/src/constants/tabOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const TAB_OPTIONS = [
{ title: "캐스퍼 일렉트릭 봇 만들기 추첨 이벤트", route: "/lottery" },
{ title: "선착순 밸런스 게임 이벤트", route: "/rush" },
];
Loading

0 comments on commit 6d68e8b

Please sign in to comment.