Refactor: 리뷰 페이지 디자인 개선 및 시간 표시 통일

This commit is contained in:
홍창기 2024-10-02 15:39:03 +09:00
parent 76fd7cf419
commit b35f67bc53
5 changed files with 76 additions and 43 deletions

View File

@ -1,14 +1,15 @@
import { Briefcase, Tag, Box, Layers } from 'lucide-react'; import { Briefcase, Tag, Box, Layers, Check, X } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import useProjectQuery from '@/queries/projects/useProjectQuery'; import useProjectQuery from '@/queries/projects/useProjectQuery';
import useAuthStore from '@/stores/useAuthStore'; import useAuthStore from '@/stores/useAuthStore';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import formatDateTime from '@/utils/formatDateTime';
import { ReviewStatus } from '@/types'; import { ReviewStatus } from '@/types';
import timeAgo from '@/utils/timeAgo';
interface ReviewItemProps { interface ReviewItemProps {
title: string; title: string;
createdTime: string; createdTime: string;
updatedTime: string;
creatorName: string; creatorName: string;
projectId: number; projectId: number;
status: ReviewStatus; status: ReviewStatus;
@ -26,6 +27,7 @@ const typeIcons: Record<'classification' | 'detection' | 'segmentation', JSX.Ele
export default function ReviewItem({ export default function ReviewItem({
title, title,
createdTime, createdTime,
updatedTime,
creatorName, creatorName,
projectId, projectId,
status, status,
@ -45,15 +47,17 @@ export default function ReviewItem({
> >
<div className="flex h-[100px] w-full items-center justify-between border-b-[0.67px] border-[#ececef] bg-[#fbfafd] p-4"> <div className="flex h-[100px] w-full items-center justify-between border-b-[0.67px] border-[#ececef] bg-[#fbfafd] p-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-sm font-semibold text-black">{title}</p> <p className="body-small-strong text-black">{title}</p>
<p className="mt-1 text-xs text-gray-500">by {creatorName}</p> <p className="caption mt-1 text-gray-500">
Created {timeAgo(createdTime)} by {creatorName}
</p>
<div className="mt-1 flex items-center"> <div className="mt-1 flex items-center">
<Briefcase className="h-3 w-3 text-gray-500" /> <Briefcase className="h-3 w-3 text-gray-500" />
<p className="ml-1 text-xs text-gray-500">{projectData?.title}</p> <p className="caption ml-1 text-gray-500">{projectData?.title}</p>
</div> </div>
{type && ( {type && (
<div <div
className="mt-1 inline-flex max-w-fit items-center gap-1 rounded-full px-3 py-1 text-xs text-white" className="caption mt-1 inline-flex max-w-fit items-center gap-1 rounded-full px-3 py-1 text-white"
style={{ backgroundColor: type.color, padding: '1px 5px' }} style={{ backgroundColor: type.color, padding: '1px 5px' }}
> >
{icon} {icon}
@ -64,7 +68,7 @@ export default function ReviewItem({
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
<div <div
className={cn( className={cn(
'rounded-full px-3 py-0.5 text-center text-xs', 'caption flex items-center gap-1 rounded-full px-3 py-0.5',
status === 'APPROVED' status === 'APPROVED'
? 'bg-green-100 text-green-600' ? 'bg-green-100 text-green-600'
: status === 'REJECTED' : status === 'REJECTED'
@ -72,9 +76,10 @@ export default function ReviewItem({
: 'bg-blue-100 text-blue-600' : 'bg-blue-100 text-blue-600'
)} )}
> >
{status === 'APPROVED' ? <Check size={12} /> : status === 'REJECTED' ? <X size={12} /> : <></>}
{status} {status}
</div> </div>
<p className="text-xs text-gray-500">Created at {formatDateTime(createdTime)}</p> <p className="caption text-gray-500">Updated {timeAgo(updatedTime)}</p>
</div> </div>
</div> </div>
</Link> </Link>

View File

@ -60,6 +60,7 @@ export default function ReviewList({
reviewId={item.reviewId} reviewId={item.reviewId}
title={item.title} title={item.title}
createdTime={item.createdAt} createdTime={item.createdAt}
updatedTime={item.updatedAt}
creatorName={item.author.nickname} creatorName={item.author.nickname}
projectId={item.projectId} projectId={item.projectId}
status={item.status} status={item.status}

View File

@ -10,6 +10,10 @@ import { Button } from '@/components/ui/button';
import 'slick-carousel/slick/slick.css'; import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick-theme.css';
import ImageWithLabels from '@/components/ImageWithLabels'; import ImageWithLabels from '@/components/ImageWithLabels';
import { cn } from '@/lib/utils';
import { Check, X } from 'lucide-react';
import formatDateTime from '@/utils/formatDateTime';
import timeAgo from '@/utils/timeAgo';
export default function ReviewDetail(): JSX.Element { export default function ReviewDetail(): JSX.Element {
const { workspaceId, projectId, reviewId } = useParams<{ const { workspaceId, projectId, reviewId } = useParams<{
@ -59,13 +63,50 @@ export default function ReviewDetail(): JSX.Element {
return ( return (
<div className="review-detail-container p-4"> <div className="review-detail-container p-4">
<div className="header mb-4"> <div className="header mb-4 flex flex-col gap-1">
<h1 className="heading mb-2">{reviewDetail.title}</h1> <h1 className="heading mb-2">{reviewDetail.title}</h1>
<p className="body-small text-gray-500"> <div className="mb-1 flex gap-1">
: {reviewDetail.author.nickname} ({reviewDetail.author.email}) <div
className={cn(
'caption mr-1 flex items-center gap-1 rounded-full px-3 py-0.5',
reviewDetail.reviewStatus === 'APPROVED'
? 'bg-green-100 text-green-600'
: reviewDetail.reviewStatus === 'REJECTED'
? 'bg-red-100 text-red-600'
: 'bg-blue-100 text-blue-600'
)}
>
{reviewDetail.reviewStatus === 'APPROVED' ? (
<Check size={12} />
) : reviewDetail.reviewStatus === 'REJECTED' ? (
<X size={12} />
) : (
<></>
)}
{reviewDetail.reviewStatus}
</div>
{reviewDetail.reviewStatus === 'APPROVED' || reviewDetail.reviewStatus === 'REJECTED' ? (
<>
<p className="body-small text-gray-500">by</p>
<p className="body-small-strong text-gray-500">
{reviewDetail.reviewer.nickname} ({reviewDetail.reviewer.email})
</p> </p>
<p className="body-small text-gray-500"> : {new Date(reviewDetail.createdAt).toLocaleDateString()}</p> </>
<p className="body-small text-gray-500"> : {new Date(reviewDetail.updatedAt).toLocaleDateString()}</p> ) : (
<p className="body-small text-gray-500">updated</p>
)}
<p className="body-small-strong text-gray-500">{timeAgo(reviewDetail.updatedAt)}</p>
<p className="body-small text-gray-500">({formatDateTime(reviewDetail.updatedAt)})</p>
</div>
<div className="flex gap-1">
<p className="body-small-strong text-gray-500">
{reviewDetail.author.nickname} ({reviewDetail.author.email})
</p>
<p className="body-small text-gray-500">requested a review</p>
<p className="body-small-strong text-gray-500">{timeAgo(reviewDetail.createdAt)}</p>
<p className="body-small text-gray-500">({formatDateTime(reviewDetail.createdAt)})</p>
</div>
</div> </div>
<div className="relative w-full"> <div className="relative w-full">
@ -111,28 +152,6 @@ export default function ReviewDetail(): JSX.Element {
)} )}
</div> </div>
{(reviewDetail.reviewStatus === 'APPROVED' || reviewDetail.reviewStatus === 'REJECTED') && (
<div className="reviewer-info mt-6">
<h2 className="text-lg font-semibold">
: {reviewDetail.reviewStatus === 'APPROVED' ? '승인됨' : '거부됨'}
</h2>
<div className="flex items-center">
<img
src={reviewDetail.reviewer.profileImage}
alt="리뷰어 프로필"
className="h-10 w-10 rounded-full"
/>
<div className="ml-4">
<p className="font-bold">{reviewDetail.reviewer.nickname}</p>
<p className="text-gray-500">{reviewDetail.reviewer.email}</p>
<p className="text-gray-500">
{reviewDetail.reviewStatus === 'APPROVED' ? '승인한 사람 : ' : '거부한 사람 : '}
{reviewDetail.reviewer.nickname}
</p>
</div>
</div>
</div>
)}
<div className="mt-6 flex justify-end gap-2"> <div className="mt-6 flex justify-end gap-2">
<Link to={`/admin/${workspaceId}/reviews`}> <Link to={`/admin/${workspaceId}/reviews`}>
<Button variant="black"> </Button> <Button variant="black"> </Button>

View File

@ -1,6 +1,7 @@
export default function formatDateTime(dateTimeString: string): string { export default function formatDateTime(dateTimeString: string): string {
const [date, time] = dateTimeString.split('T'); const [date, time] = dateTimeString.split('T');
const [hours, minutes] = time.split(':'); const [year, month, day] = date.split('-');
const [hours, minutes, seconds] = time.split(':');
return `${date} ${hours}:${minutes}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} }

View File

@ -1,13 +1,20 @@
export default function timeAgo(date: string | Date) { export default function timeAgo(date: string | Date) {
const now = new Date(); const now = new Date();
const past = new Date(date); const past = new Date(date);
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
if (diffInSeconds < 60) return `${Math.max(diffInSeconds, 0)}초 전`; const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
if (diffInSeconds === 1) return `${Math.max(diffInSeconds, 0)} second ago`;
if (diffInSeconds < 60) return `${Math.max(diffInSeconds, 0)} seconds ago`;
const diffInMinutes = Math.floor(diffInSeconds / 60); const diffInMinutes = Math.floor(diffInSeconds / 60);
if (diffInMinutes < 60) return `${diffInMinutes}분 전`; if (diffInMinutes === 1) return `${diffInMinutes} minute ago`;
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
const diffInHours = Math.floor(diffInMinutes / 60); const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) return `${diffInHours}시간 전`; if (diffInHours === 1) return `${diffInHours} hour ago`;
if (diffInHours < 24) return `${diffInHours} hours ago`;
const diffInDays = Math.floor(diffInHours / 24); const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays}일 전`; if (diffInDays === 1) return `${diffInDays} day ago`;
return `${diffInDays} days ago`;
} }