Skip to content

Commit

Permalink
[Feature] 임시 스터디원 관리 UI, API 연결 (#140)
Browse files Browse the repository at this point in the history
* feat: fetcher에 임시 에러코드 토스트 추가

* feat: 어드민, 클라이언트에 css 및 토스트 컨테이너 추가

* feat: participants -> students url 변경 및 헤더 구현

* chore: 불필요한 예시 모달 삭제

* feat: 스터디리스트 드롭다운 api 연결

* feat: 임시 table UI

* feat: 스터디 수강생 명단 api 등록

* feat: 스터디 수강생 api 연결

* fix: thead tbody 추가

* feat: studyAtom값으로 드롭다운 조절

* feat: studyList 상위에서 내려주기

* fix: 변경된 MyStudyList API 수정

* feat: 스터디 수강생이 없을 때

* chore: dto 네이밍 변경, studentList 초기값 변경

* chore: 불필요한 typo prop 삭제, style 객체 분리

* rename: StudyProvider 위치 변경

* chore: 타입 명시 삭제

* chore: PropsWithChildren 타입 사용

* rename: StudyDropDown 폴더 관리

* feat: 타이틀 줄바꿈 방지 및 스타일 분리

* refactor: useFetchStudents 커스텀 훅 분리

* feat: 담당한 스터디가 없을 때

* chore: study -> selectedStudy 네이밍 변경
  • Loading branch information
hamo-o authored Sep 8, 2024
1 parent 88ec4e7 commit 056d297
Show file tree
Hide file tree
Showing 19 changed files with 359 additions and 41 deletions.
11 changes: 11 additions & 0 deletions apps/admin/apis/study/studyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import type { AttendanceApiResponseDto } from "types/dtos/attendance";
import type { CurriculumApiResponseDto } from "types/dtos/curriculumList";
import type { StudyBasicInfoApiResponseDto } from "types/dtos/studyBasicInfo";
import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent";
import type { StudyAnnouncementType } from "types/entities/study";

import type { StudyListApiResponseDto } from "../../types/dtos/studyList";
Expand Down Expand Up @@ -148,4 +149,14 @@ export const studyApi = {
);
return response.data;
},
getStudyStudents: async (studyId: number) => {
const response = await fetcher.get<StudyStudentApiResponseDto[]>(
`/mentor/studies/${studyId}/students`,
{
next: { tags: [tags.students] },
cache: "force-cache",
}
);
return response.data;
},
};
23 changes: 0 additions & 23 deletions apps/admin/app/@modal/(.)participants/page.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/admin/app/@modal/default.tsx

This file was deleted.

6 changes: 6 additions & 0 deletions apps/admin/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const RootLayout = ({
limit={1}
/>
<JotaiProvider>
<ToastContainer
hideProgressBar
autoClose={4000}
closeButton={false}
limit={1}
/>
{children}
{modal}
</JotaiProvider>
Expand Down
5 changes: 0 additions & 5 deletions apps/admin/app/participants/page.tsx

This file was deleted.

50 changes: 50 additions & 0 deletions apps/admin/app/students/_components/StudentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { css } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
import { Text } from "@wow-class/ui";
import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent";

import StudentListItem from "./StudentListItem";

const StudentList = ({
studentList,
}: {
studentList: StudyStudentApiResponseDto[] | [];
}) => {
if (!studentList.length) return <Text>스터디 수강생이 없어요.</Text>;

return (
<styled.table borderCollapse="collapse">
<thead>
<styled.tr borderBottom="1px solid" borderColor="sub">
<Text as="th" className={tableThStyle} typo="body2">
이름
</Text>
<Text as="th" className={tableThStyle} typo="body2">
학번
</Text>
<Text as="th" className={tableThStyle} typo="body2">
디스코드 사용자명
</Text>
<Text as="th" className={tableThStyle} typo="body2">
디스코드 닉네임
</Text>
<Text as="th" className={tableThStyle} typo="body2">
깃허브 링크
</Text>
</styled.tr>
</thead>
<tbody>
{studentList.map((student) => (
<StudentListItem key={student.memberId} {...student} />
))}
</tbody>
</styled.table>
);
};

const tableThStyle = css({
padding: "1rem",
textAlign: "left",
});

export default StudentList;
51 changes: 51 additions & 0 deletions apps/admin/app/students/_components/StudentListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { css } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
import { Text } from "@wow-class/ui";
import Link from "next/link";
import type { CSSProperties } from "react";
import type { StudyStudentApiResponseDto } from "types/dtos/studyStudent";
import TextButton from "wowds-ui/TextButton";

const StudentListItem = ({
name,
studentId,
discordUsername,
nickname,
githubLink,
}: StudyStudentApiResponseDto) => {
return (
<styled.tr>
<Text as="td" className={tableThStyle}>
{name}
</Text>
<Text as="td" className={tableThStyle}>
{studentId}
</Text>
<Text as="td" className={tableThStyle}>
{discordUsername}
</Text>
<Text as="td" className={tableThStyle}>
{nickname}
</Text>
<Text as="td" className={tableThStyle}>
<TextButton
asProp={Link}
href={githubLink || ""}
style={textButtonStyle}
text={githubLink}
/>
</Text>
</styled.tr>
);
};

const tableThStyle = css({
padding: "1rem",
});

const textButtonStyle: CSSProperties = {
width: "fit-content",
padding: 0,
};

export default StudentListItem;
28 changes: 28 additions & 0 deletions apps/admin/app/students/_components/StudentsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Text } from "@wow-class/ui";
import ItemSeparator from "components/ItemSeparator";
import type { CSSProperties } from "react";
import type { StudyListApiResponseDto } from "types/dtos/studyList";

import StudyDropDown from "./StudyDropDown";

const StudentsHeader = ({
studyList,
}: {
studyList: StudyListApiResponseDto[];
}) => {
return (
<Text as="h1" style={titleStyle} typo="h1">
수강생 관리 <ItemSeparator height={6} width={6} />
<StudyDropDown studyList={studyList} />
</Text>
);
};

const titleStyle: CSSProperties = {
display: "flex",
alignItems: "center",
gap: "0.75rem",
whiteSpace: "nowrap",
};

export default StudentsHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Flex } from "@styled-system/jsx";
import { DownArrow } from "wowds-icons";

const DropDownTrigger = () => {
return (
<Flex
align="center"
background="backgroundAlternative"
borderRadius={9999}
height={24}
justify="center"
width={24}
>
<DownArrow stroke="sub" />
</Flex>
);
};

export default DropDownTrigger;
53 changes: 53 additions & 0 deletions apps/admin/app/students/_components/StudyDropDown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Flex } from "@styled-system/jsx";
import { useAtom } from "jotai";
import type { ReactNode } from "react";
import type { StudyListApiResponseDto } from "types/dtos/studyList";
import DropDown from "wowds-ui/DropDown";
import DropDownOption from "wowds-ui/DropDownOption";

import { studyAtom } from "../../_contexts/StudyProvider";
import DropDownTrigger from "./DropDownTrigger";

const StudyDropDown = ({
studyList,
}: {
studyList: StudyListApiResponseDto[];
}) => {
const [study, setStudy] = useAtom(studyAtom);

if (!study) return null;

return (
<DropDown
trigger={
<Flex
align="center"
color="primary"
cursor="pointer"
gap="sm"
width="20rem"
>
{study.title}
<DropDownTrigger />
</Flex>
}
onChange={(value: { selectedValue: string; selectedText: ReactNode }) => {
setStudy({
studyId: +value.selectedValue,
title: value.selectedText,
});
}}
>
{studyList.map((study: StudyListApiResponseDto) => (
<DropDownOption
key={study.studyId}
style={{ cursor: "pointer" }}
text={study.title}
value={study.studyId.toString()}
/>
))}
</DropDown>
);
};

export default StudyDropDown;
18 changes: 18 additions & 0 deletions apps/admin/app/students/_contexts/StudyProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { atom, createStore, Provider } from "jotai";
import type { PropsWithChildren, ReactNode } from "react";

const studyIdStore = createStore();

export type StudyAtomprops = {
studyId: number;
title: ReactNode;
};

export const studyAtom = atom<StudyAtomprops>();
studyIdStore.set(studyAtom, undefined);

export const StudyProvider = ({ children }: PropsWithChildren) => {
return <Provider store={studyIdStore}>{children}</Provider>;
};
30 changes: 30 additions & 0 deletions apps/admin/app/students/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { css } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
import Navbar from "components/Navbar";

import { StudyProvider } from "./_contexts/StudyProvider";

const StudentsLayout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return (
<StudyProvider>
<Navbar />
<styled.main className={StudentsLayoutStyle}>{children}</styled.main>
</StudyProvider>
);
};

export default StudentsLayout;

const StudentsLayoutStyle = css({
display: "flex",
flexDirection: "column",
gap: "sm",
height: "100vh",
overflow: "scroll",
width: "100%",
padding: "54px 101px",
});
49 changes: 49 additions & 0 deletions apps/admin/app/students/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { Flex } from "@styled-system/jsx";
import { Text } from "@wow-class/ui";
import { studyApi } from "apis/study/studyApi";
import useFetchStudents from "hooks/fetch/useFetchStudents";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import type { StudyListApiResponseDto } from "types/dtos/studyList";
import isAdmin from "utils/isAdmin";

import StudentList from "./_components/StudentList";
import StudentsHeader from "./_components/StudentsHeader";
import { studyAtom } from "./_contexts/StudyProvider";

const StudentsPage = () => {
const [studyList, setStudyList] = useState<StudyListApiResponseDto[]>();
const [selectedStudy, setSelectedStudy] = useAtom(studyAtom);

useEffect(() => {
const fetchData = async () => {
const adminStatus = await isAdmin();
if (adminStatus) {
const data = adminStatus
? await studyApi.getStudyList()
: await studyApi.getMyStudyList();

if (data && data.length && data[0]) {
setStudyList(data);
setSelectedStudy({ studyId: data[0].studyId, title: data[0].title });
}
}
};

fetchData();
}, [setSelectedStudy]);

const student = useFetchStudents(selectedStudy);
if (!studyList) return <Text>담당한 스터디가 없어요.</Text>;

return (
<Flex direction="column" gap="3rem">
<StudentsHeader studyList={studyList} />
<StudentList studentList={student.studentList} />
</Flex>
);
};

export default StudentsPage;
12 changes: 6 additions & 6 deletions apps/admin/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ const Navbar = async () => {
};
}),
},
// {
// href: "participants",
// imageUrl: participantImageUrl,
// alt: "participant-icon",
// name: "수강생 관리",
// },
{
href: "/students",
imageUrl: participantImageUrl,
alt: "participant-icon",
name: "수강생 관리",
},
];

return (
Expand Down
Loading

0 comments on commit 056d297

Please sign in to comment.