Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fe): create admin instructor management page #2341

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
45 changes: 45 additions & 0 deletions apps/frontend/app/admin/_components/GroupAdminSideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import { Separator } from '@/components/shadcn/separator'
import { cn } from '@/libs/utils'
import type { Route } from 'next'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import type { IconType } from 'react-icons'
import { FaChartBar, FaPen } from 'react-icons/fa6'
import { GroupLink } from './GroupLink'

export function GroupAdminSideBar() {
const pathname = usePathname()

const navItems: { name: string; path: Route; icon: IconType }[] = [
{ name: 'Dashboard', path: '/admin', icon: FaChartBar },
{ name: 'Problem', path: '/admin/problem', icon: FaPen }
]

return (
<div className="flex flex-col gap-2">
{navItems.map((item) => (
<Link
key={item.name}
href={item.path}
className={cn(
'rounded px-4 py-2 transition',
(
item.path === '/admin'
? pathname === item.path
: pathname.startsWith(item.path)
)
? 'bg-primary text-white hover:opacity-95'
: 'text-slate-600 hover:bg-slate-100'
)}
>
{item.icon && <item.icon className="mr-2 inline-block" />}
{item.name}
</Link>
))}
<Separator className="my-4 transition" />
<GroupLink />
</div>
)
}
53 changes: 53 additions & 0 deletions apps/frontend/app/admin/_components/GroupLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { GET_GROUPS } from '@/graphql/group/queries'
import { cn } from '@/libs/utils'
import { useQuery } from '@apollo/client'
import type { Route } from 'next'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { FaUserGroup } from 'react-icons/fa6'

export function GroupLink() {
const pathname = usePathname()

const { data } = useQuery(GET_GROUPS, {
variables: {
cursor: 1,
take: 5
}
})
return (
<div
className={cn(
'flex flex-col gap-2 hover:bg-slate-100',
pathname.startsWith('/admin/group') && 'bg-slate-100 hover:opacity-95'
)}
>
<Link
href={'/admin/group/' as Route}
className={cn(
'rounded px-4 py-2 transition',
pathname === '/admin/group'
? 'bg-primary text-white hover:opacity-95'
: 'text-slate-600 hover:bg-slate-200'
)}
>
{<FaUserGroup className="mr-2 inline-block" />}
Group
</Link>
{data?.getGroups.map((group) => (
<Link
href={`/admin/group/${group.id}` as Route}
key={group.id}
className={cn(
'rounded py-2 pl-8 pr-4 transition',
pathname.startsWith(`/admin/group/${group.id}`)
? 'bg-primary text-white hover:opacity-95'
: 'text-slate-600 hover:bg-slate-200'
)}
>
{group.groupName}
</Link>
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DateTimePickerDemo } from '@/components/shadcn/date-time-picker-demo'
import { useController, useFormContext } from 'react-hook-form'
import { ErrorMessage } from '../../_components/ErrorMessage'
import { ErrorMessage } from './ErrorMessage'

export function TimeForm({ name }: { name: string }) {
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { toast } from 'sonner'
import { useDataTable } from './context'

interface DataTableDeleteButtonProps<TData extends { id: number }, TPromise> {
target: 'problem' | 'contest'
target: 'problem' | 'contest' | 'assignment' | 'group'
deleteTarget: (id: number) => Promise<TPromise>
getCanDelete?: (selectedRows: TData[]) => Promise<boolean>
onSuccess?: () => void
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/app/admin/contest/[contestId]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { FaAngleLeft } from 'react-icons/fa6'
import { IoIosCheckmarkCircle } from 'react-icons/io'
import { TimeForm } from '../../../_components/TimeForm'
import { ContestProblemListLabel } from '../../_components/ContestProblemListLabel'
import { ContestProblemTable } from '../../_components/ContestProblemTable'
import { ImportDialog } from '../../_components/ImportDialog'
import { TimeForm } from '../../_components/TimeForm'
import { type ContestProblem, editSchema } from '../../_libs/schemas'
import { EditContestForm } from './_components/EditContestForm'

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/app/admin/contest/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { ConfirmNavigation } from '../../_components/ConfirmNavigation'
import { DescriptionForm } from '../../_components/DescriptionForm'
import { FormSection } from '../../_components/FormSection'
import { SwitchField } from '../../_components/SwitchField'
import { TimeForm } from '../../_components/TimeForm'
import { TitleForm } from '../../_components/TitleForm'
import { ContestProblemListLabel } from '../_components/ContestProblemListLabel'
import { ContestProblemTable } from '../_components/ContestProblemTable'
import { ImportDialog } from '../_components/ImportDialog'
import { TimeForm } from '../_components/TimeForm'
import type { ContestProblem } from '../_libs/schemas'
import { CreateContestForm } from './_components/CreateContestForm'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client'

import { cn } from '@/libs/utils'
import type { Route } from 'next'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import type { IconType } from 'react-icons'
import {
MdAssignment,
MdHome,
MdNotifications,
MdRateReview,
MdOutlineQuestionAnswer,
MdEditDocument,
MdPeople
} from 'react-icons/md'

interface GroupSideBarProps {
groupId: string
}

export function GroupSideBar({ groupId }: GroupSideBarProps) {
const pathname = usePathname()

const navItems: { name: string; path: string; icon: IconType }[] = [
{ name: 'Home', path: `/admin/group/${groupId}`, icon: MdHome },
{
name: 'Notice',
path: `/admin/group/${groupId}/notice`,
icon: MdNotifications
},
{
name: 'User',
path: `/admin/group/${groupId}/user`,
icon: MdPeople
},
{
name: 'Assignment',
path: `/admin/group/${groupId}/assignment`,
icon: MdAssignment
},
{
name: 'Exam',
path: `/admin/group/${groupId}/exam`,
icon: MdEditDocument
},
{
name: 'Grade',
path: `/admin/group/${groupId}/grade`,
icon: MdRateReview
},
{
name: 'Q&A',
path: `/admin/group/${groupId}/qna`,
icon: MdOutlineQuestionAnswer
}
]

return (
<div className="flex flex-col gap-2">
{navItems.map((item) => (
<Link
key={item.name}
href={item.path as Route}
className={cn(
'rounded px-2 py-2 transition',
pathname === item.path
? 'bg-primary text-white hover:opacity-95'
: 'text-slate-600 hover:bg-slate-100'
)}
>
{item.icon && <item.icon className="mr-2 inline-block h-5 w-5" />}
{item.name}
</Link>
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client'

import { OptionSelect } from '@/app/admin/_components/OptionSelect'
import { ContainedContests } from '@/app/admin/problem/_components/ContainedContests'
import { Badge } from '@/components/shadcn/badge'
import { Input } from '@/components/shadcn/input'
import type { Level } from '@/types/type'
import type { ColumnDef } from '@tanstack/react-table'
import { toast } from 'sonner'
import type { AssignmentProblem } from '../_libs/schemas'

export const createColumns = (
setProblems: React.Dispatch<React.SetStateAction<AssignmentProblem[]>>,
disableInput: boolean
): ColumnDef<AssignmentProblem>[] => [
// {
// id: 'select',
// header: ({ table }) => (
// <Checkbox
// checked={
// table.getIsAllPageRowsSelected() ||
// (table.getIsSomePageRowsSelected() && 'indeterminate')
// }
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
// aria-label="Select all"
// className="translate-y-[2px] bg-white"
// />
// ),
// cell: ({ row }) => (
// <Checkbox
// onClick={(e) => e.stopPropagation()}
// checked={row.getIsSelected()}
// onCheckedChange={(value) => row.toggleSelected(!!value)}
// aria-label="Select row"
// className="translate-y-[2px] bg-white"
// />
// ),
// enableSorting: false,
// enableHiding: false
// },
{
accessorKey: 'title',
header: () => <p className="w-[350px] text-left text-sm">Title</p>,
cell: ({ row }) => (
<p className="w-[350px] overflow-hidden text-ellipsis whitespace-nowrap text-left">
{row.getValue('title')}
</p>
),
footer: () => <p className="w-[350px] text-left text-sm">Score Sum</p>,
enableSorting: false,
enableHiding: false
},
{
accessorKey: 'score',
header: () => <p className="text-center text-sm">Score</p>,
cell: ({ row }) => (
<div
className="flex justify-center"
onClick={() => {
if (disableInput) {
toast.error('Problem scoring cannot be edited')
}
}}
>
<Input
disabled={disableInput}
defaultValue={row.getValue('score')}
className="hide-spin-button w-[70px] focus-visible:ring-0 disabled:pointer-events-none"
type="number"
min={0}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
const target = e.target as HTMLInputElement
target.blur()
}
}}
onBlur={(event) => {
setProblems((prevProblems: AssignmentProblem[]) =>
prevProblems.map((problem) =>
problem.id === row.original.id
? { ...problem, score: Number(event.target.value) }
: problem
)
)
}}
/>
</div>
),
footer: ({ table }) => (
<div className="flex justify-center">
<Input
disabled={true}
className="w-[70px] focus-visible:ring-0"
value={table
.getCoreRowModel()
.rows.map((row) => row.original)
.reduce((total, problem) => total + problem.score, 0)}
/>
</div>
),
enableSorting: false,
enableHiding: false
},
{
accessorKey: 'order',
header: () => <p className="text-center text-sm">Order</p>,
cell: ({ table, row }) => {
const tableRows = table.getRowModel().rows
const alphabetArray = tableRows.map((_, index) =>
String.fromCharCode(65 + index)
)
return (
<div
className="flex justify-center"
onClick={() => {
if (disableInput) {
toast.error('Problem order cannot be edited')
}
}}
>
<OptionSelect
value={
row.original.order !== undefined
? String.fromCharCode(Number(65 + row.original.order))
: String.fromCharCode(Number(65 + row.index))
}
options={alphabetArray}
onChange={(selectedOrder) => {
setProblems((prevProblems: AssignmentProblem[]) =>
prevProblems.map((problem) =>
problem.id === row.original.id
? { ...problem, order: selectedOrder.charCodeAt(0) - 65 }
: problem
)
)
}}
className="w-[70px] disabled:pointer-events-none"
disabled={disableInput}
/>
</div>
)
}
},
{
accessorKey: 'difficulty',
header: () => <p className="text-center text-sm">Level</p>,
cell: ({ row }) => {
const level: string = row.getValue('difficulty')
const formattedLevel = `Level ${level.slice(-1)}`
return (
<div>
<Badge
variant={level as Level}
className="whitespace-nowrap rounded-md px-1.5 py-1 font-normal"
>
{formattedLevel}
</Badge>
</div>
)
},
enableSorting: false
},
{
accessorKey: 'included',
header: () => <p className="text-center text-sm">Included</p>,
cell: ({ row }) => (
<div className="flex justify-center">
{/* 백엔드 API 작업 완료 후 수정 예정 */}
<ContainedContests problemId={row.original.id} />
</div>
),
enableSorting: false
}
]
Loading