-
Notifications
You must be signed in to change notification settings - Fork 16
feat(fe): make notice page #3514
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
Changes from 1 commit
88b66fd
9e35026
82adec6
7d9e397
a2ed1a0
b090163
be9d50a
9ba210f
e8bbe8a
ca4902c
41ed0c7
4be3d17
405ba3c
6189e53
832cd95
685c149
19b993b
06d55d8
9c1a1ac
58f17f6
63be054
9ab8e52
e717483
af757ba
a754e5f
cc2cc12
4eada12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| 'use client' | ||
|
|
||
| import { cn, dateFormatter } from '@/libs/utils' | ||
| import type { ColumnDef } from '@tanstack/react-table' | ||
|
|
||
| export interface CourseNoticeRow { | ||
| id: number | ||
| no: string | ||
| title: string | ||
| createdBy: string | ||
| date: string | ||
| isRead: boolean | ||
| isFixed: boolean | ||
| } | ||
|
|
||
| export const courseNoticeColumns: ColumnDef<CourseNoticeRow>[] = [ | ||
| { | ||
| accessorKey: 'no', | ||
| header: 'NO', | ||
| cell: ({ row }) => ( | ||
| <div | ||
| className={cn( | ||
| 'relative w-full text-center text-sm text-[#666666]', | ||
| row.original.isFixed && | ||
| "before:bg-primary before:absolute before:left-[-16px] before:top-[-18px] before:h-[57px] before:w-[3px] before:rounded-full before:content-['']" | ||
| )} | ||
| > | ||
| {row.original.no} | ||
| </div> | ||
| ), | ||
| enableSorting: false | ||
| }, | ||
| { | ||
| accessorKey: 'title', | ||
| header: 'Title', | ||
| cell: ({ row }) => ( | ||
| <div className="flex items-center justify-start gap-2 overflow-hidden text-sm text-black"> | ||
| <span className="line-clamp-1">{row.original.title}</span> | ||
| {!row.original.isRead && ( | ||
| <span className="bg-primary h-[6px] w-[6px] shrink-0 rounded-full" /> | ||
| )} | ||
| </div> | ||
| ), | ||
| enableSorting: false | ||
| }, | ||
| { | ||
| accessorKey: 'date', | ||
| header: 'Date', | ||
| cell: ({ row }) => ( | ||
| <span className="text-sm text-[#666666]"> | ||
| {row.original.date | ||
| ? dateFormatter(row.original.date, 'YY-MM-DD HH:mm') | ||
| : '-'} | ||
| </span> | ||
| ), | ||
| enableSorting: false | ||
| }, | ||
| { | ||
| accessorKey: 'createdBy', | ||
| header: 'Writer', | ||
| cell: ({ row }) => ( | ||
| <span className="text-sm text-[#666666]">{row.original.createdBy}</span> | ||
|
egg-zz marked this conversation as resolved.
|
||
| ), | ||
| enableSorting: false | ||
| } | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| 'use client' | ||
|
|
||
| import { | ||
| DataTable, | ||
| DataTablePagination, | ||
| DataTableRoot | ||
| } from '@/app/admin/_components/table' | ||
| import { | ||
| DropdownMenu, | ||
| DropdownMenuContent, | ||
| DropdownMenuItem, | ||
| DropdownMenuTrigger | ||
| } from '@/components/shadcn/dropdown-menu' | ||
| import { cn } from '@/libs/utils' | ||
| import arrowDownIcon from '@/public/icons/arrow-down.svg' | ||
| import type { CourseNoticeListItem } from '@/types/type' | ||
| import { useQuery } from '@tanstack/react-query' | ||
| import Image from 'next/image' | ||
| import { useMemo, useState } from 'react' | ||
| import { mockCourseNotices } from '../notice/_components/mock' | ||
| import { | ||
| courseNoticeColumns, | ||
| type CourseNoticeRow | ||
| } from './CourseNoticeColumns' | ||
|
|
||
| type FilterType = 'all' | 'unread' | ||
| type OrderType = 'latest' | 'oldest' | ||
|
|
||
| interface CourseNoticeTableProps { | ||
| courseId: number | ||
| } | ||
|
|
||
| const getTime = (notice: CourseNoticeListItem) => | ||
| new Date(notice.createTime ?? notice.updateTime ?? 0).getTime() | ||
|
|
||
| export function CourseNoticeTable({ courseId }: CourseNoticeTableProps) { | ||
| const [filterType, setFilterType] = useState<FilterType>('all') | ||
| const [orderType, setOrderType] = useState<OrderType | undefined>() | ||
|
|
||
| let orderLabel = 'Order' | ||
|
|
||
| if (orderType === 'latest') { | ||
| orderLabel = 'Latest' | ||
| } else if (orderType === 'oldest') { | ||
| orderLabel = 'Oldest' | ||
| } | ||
|
|
||
| const { data: notices = [] } = useQuery<CourseNoticeListItem[]>({ | ||
| queryKey: ['courseNotices', courseId, filterType, orderType], | ||
| queryFn: () => mockCourseNotices, | ||
| enabled: Boolean(courseId), | ||
| retry: false | ||
| }) | ||
|
|
||
| const tableData: CourseNoticeRow[] = useMemo(() => { | ||
| const filteredNotices = | ||
| filterType === 'unread' | ||
| ? notices.filter((notice) => !notice.isRead) | ||
| : notices | ||
|
|
||
| const noMap = new Map( | ||
| [...filteredNotices] | ||
| .sort((a, b) => getTime(a) - getTime(b)) | ||
| .map((notice, index) => [notice.id, index + 1]) | ||
| ) | ||
|
|
||
| return [...filteredNotices] | ||
| .sort((a, b) => { | ||
| if (a.isFixed !== b.isFixed) { | ||
| return a.isFixed ? -1 : 1 | ||
| } | ||
| return orderType === 'oldest' | ||
| ? getTime(a) - getTime(b) | ||
| : getTime(b) - getTime(a) | ||
| }) | ||
| .map((notice) => ({ | ||
| id: notice.id, | ||
| no: String(noMap.get(notice.id) ?? 0).padStart(2, '0'), | ||
| title: notice.title, | ||
| createdBy: notice.createdBy ?? 'Unknown', | ||
| date: notice.createTime ?? notice.updateTime ?? '', | ||
| isRead: notice.isRead, | ||
| isFixed: notice.isFixed | ||
| })) | ||
| }, [notices, filterType, orderType]) | ||
|
Comment on lines
+79
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic inside this
This can be optimized by reducing the number of sorts. For instance, you could perform the final sort first, and then map over the sorted array to generate the |
||
|
|
||
| return ( | ||
| <DataTableRoot | ||
| data={tableData} | ||
| columns={courseNoticeColumns} | ||
| defaultPageSize={10} | ||
| defaultSortState={[]} | ||
| > | ||
| <div className="mb-6 flex items-center justify-between"> | ||
| <span className="text-2xl font-semibold leading-[33.6px] tracking-[-0.48px]"> | ||
| NOTICE | ||
| </span> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <button | ||
| type="button" | ||
| className="flex h-[46px] min-w-[108px] items-center justify-center gap-2 rounded-full border bg-white text-sm leading-[22.4px] text-neutral-500 outline-none" | ||
| > | ||
| <span>{orderLabel}</span> | ||
| <Image | ||
| src={arrowDownIcon} | ||
| alt="arrow down" | ||
| className="h-4 w-4" | ||
| /> | ||
| </button> | ||
| </DropdownMenuTrigger> | ||
|
|
||
| <DropdownMenuContent | ||
| align="end" | ||
| className="border-neutral-95 min-w-[108px] rounded-[16px] border bg-white p-1" | ||
| > | ||
| <DropdownMenuItem | ||
| onClick={() => setOrderType('latest')} | ||
| className="cursor-pointer rounded-[10px] text-sm leading-[22.4px] text-neutral-500" | ||
| > | ||
| Latest | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem | ||
| onClick={() => setOrderType('oldest')} | ||
| className="cursor-pointer rounded-[10px] text-sm leading-[22.4px] text-neutral-500" | ||
| > | ||
| Oldest | ||
| </DropdownMenuItem> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
|
|
||
| <div className="flex h-[46px] items-center rounded-full border p-[5px]"> | ||
| {(['all', 'unread'] as const).map((type) => ( | ||
| <button | ||
| key={type} | ||
| type="button" | ||
| onClick={() => setFilterType(type)} | ||
| className={cn( | ||
| 'text-body1_m_16 h-9 rounded-full px-8 py-[6px]', | ||
| filterType === type | ||
| ? 'bg-primary text-white' | ||
| : 'text-[#808080]' | ||
|
egg-zz marked this conversation as resolved.
|
||
| )} | ||
| > | ||
| {type === 'all' ? 'All' : 'Unread'} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <DataTable | ||
| size="md" | ||
| headerStyle={{ | ||
| no: 'w-[80px]', | ||
| title: '', | ||
| date: 'w-[180px]', | ||
| createdBy: 'w-[110px]' | ||
| }} | ||
| bodyStyle={{ | ||
| no: 'text-center', | ||
| title: 'justify-start', | ||
| date: 'text-center', | ||
| createdBy: 'text-center' | ||
| }} | ||
| getHref={(row) => `/course/${courseId}/notice/${row.id}`} | ||
| /> | ||
|
|
||
| <div className="mt-10"> | ||
| <DataTablePagination showRowsPerPage={false} /> | ||
| </div> | ||
| </DataTableRoot> | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These
before:pseudo-element styles are complex and contain magic numbers (e.g.,left-[-16px],top-[-18px]). This makes the code hard to read and maintain. Consider extracting this into a separate, well-named utility class in your Tailwind configuration for better readability and reusability.