[resumes][refactor] Update resume review page UI (#418)

* [resumes][refactor] update comments ui

* [resumes][refactor] change comment border color

* [resumes][refactor] update review ui

* [resumes][refactor] rearrange review page

* update add review button

Co-authored-by: Terence Ho <>
pull/422/head
Terence 2 years ago committed by GitHub
parent 70b102f87e
commit 3f6ae58374
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react'; import { useState } from 'react';
import { ChevronUpIcon } from '@heroicons/react/20/solid'; import { ChevronUpIcon } from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline'; import { FaceSmileIcon } from '@heroicons/react/24/outline';
@ -26,12 +27,7 @@ export default function ResumeCommentListItem({
const [showReplies, setShowReplies] = useState(true); const [showReplies, setShowReplies] = useState(true);
return ( return (
<div <div className="min-w-fit">
className={clsx(
'min-w-fit rounded-md bg-white ',
!comment.parentId &&
'border-primary-300 w-11/12 border-2 p-2 drop-shadow-md',
)}>
<div className="flex flex-row space-x-2 p-1 align-top"> <div className="flex flex-row space-x-2 p-1 align-top">
{/* Image Icon */} {/* Image Icon */}
{comment.user.image ? ( {comment.user.image ? (
@ -58,7 +54,7 @@ export default function ResumeCommentListItem({
<div className="flex flex-row items-center space-x-1"> <div className="flex flex-row items-center space-x-1">
<p <p
className={clsx( className={clsx(
'font-medium text-black', 'font-medium text-gray-800',
!!comment.parentId && 'text-sm', !!comment.parentId && 'text-sm',
)}> )}>
{comment.user.name ?? 'Reviewer ABC'} {comment.user.name ?? 'Reviewer ABC'}
@ -72,9 +68,8 @@ export default function ResumeCommentListItem({
</div> </div>
<div className="px-2 text-xs text-slate-600"> <div className="px-2 text-xs text-slate-600">
{comment.createdAt.toLocaleString('en-US', { {formatDistanceToNow(comment.createdAt, {
dateStyle: 'medium', addSuffix: true,
timeStyle: 'short',
})} })}
</div> </div>
</div> </div>
@ -86,10 +81,12 @@ export default function ResumeCommentListItem({
setIsEditingComment={setIsEditingComment} setIsEditingComment={setIsEditingComment}
/> />
) : ( ) : (
<ResumeExpandableText <div className="text-gray-800">
key={comment.description} <ResumeExpandableText
text={comment.description} key={comment.description}
/> text={comment.description}
/>
</div>
)} )}
{/* Upvote and edit */} {/* Upvote and edit */}
@ -143,7 +140,15 @@ export default function ResumeCommentListItem({
!showReplies && 'rotate-180 transform', !showReplies && 'rotate-180 transform',
)} )}
/> />
<span>{showReplies ? 'Hide replies' : 'Show replies'}</span> <span>
{showReplies
? `Hide ${
comment.children.length === 1 ? 'reply' : 'replies'
}`
: `Show ${comment.children.length} ${
comment.children.length === 1 ? 'reply' : 'replies'
}`}
</span>
</button> </button>
{showReplies && ( {showReplies && (
@ -152,7 +157,7 @@ export default function ResumeCommentListItem({
<div className="flex-grow border-r border-slate-300" /> <div className="flex-grow border-r border-slate-300" />
</div> </div>
<div className="flex flex-col space-y-1"> <div className="flex flex-1 flex-col space-y-1">
{comment.children.map((child) => { {comment.children.map((child) => {
return ( return (
<ResumeCommentListItem <ResumeCommentListItem

@ -83,7 +83,7 @@ export default function ResumeCommentsForm({
}; };
return ( return (
<div className="h-[calc(100vh-13rem)] overflow-y-auto"> <div className="h-[calc(100vh-13rem)] overflow-y-auto pb-4">
<h2 className="text-xl font-semibold text-slate-800">Add your review</h2> <h2 className="text-xl font-semibold text-slate-800">Add your review</h2>
<p className="text-slate-800"> <p className="text-slate-800">
Please fill in at least one section to submit your review Please fill in at least one section to submit your review

@ -1,7 +1,9 @@
import clsx from 'clsx';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { import {
BookOpenIcon, BookOpenIcon,
BriefcaseIcon, BriefcaseIcon,
ChatBubbleLeftRightIcon,
CodeBracketSquareIcon, CodeBracketSquareIcon,
FaceSmileIcon, FaceSmileIcon,
IdentificationIcon, IdentificationIcon,
@ -9,24 +11,20 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { ResumesSection } from '@prisma/client'; import { ResumesSection } from '@prisma/client';
import { Spinner } from '@tih/ui'; import { Spinner } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import { RESUME_COMMENTS_SECTIONS } from './resumeCommentConstants'; import { RESUME_COMMENTS_SECTIONS } from './resumeCommentConstants';
import ResumeCommentListItem from './ResumeCommentListItem'; import ResumeCommentListItem from './ResumeCommentListItem';
import ResumeSignInButton from '../shared/ResumeSignInButton';
import type { ResumeComment } from '~/types/resume-comments'; import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentsListProps = Readonly<{ type ResumeCommentsListProps = Readonly<{
resumeId: string; resumeId: string;
setShowCommentsForm: (show: boolean) => void;
}>; }>;
export default function ResumeCommentsList({ export default function ResumeCommentsList({
resumeId, resumeId,
setShowCommentsForm,
}: ResumeCommentsListProps) { }: ResumeCommentsListProps) {
const { data: sessionData } = useSession(); const { data: sessionData } = useSession();
@ -50,31 +48,14 @@ export default function ResumeCommentsList({
} }
}; };
const renderButton = () => {
if (sessionData === null) {
return <ResumeSignInButton text="to join discussion" />;
}
return (
<Button
className="-mb-2"
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{renderButton()}
{commentsQuery.isLoading ? ( {commentsQuery.isLoading ? (
<div className="col-span-10 pt-4"> <div className="col-span-10 pt-4">
<Spinner display="block" size="lg" /> <Spinner display="block" size="lg" />
</div> </div>
) : ( ) : (
<div className="m-2 flow-root h-[calc(100vh-17rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden pt-14 pb-6"> <div className="mb-8 flow-root h-[calc(100vh-13rem)] w-full flex-col space-y-4 overflow-y-auto overflow-x-hidden">
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => { {RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
const comments = commentsQuery.data const comments = commentsQuery.data
? commentsQuery.data.filter((comment: ResumeComment) => { ? commentsQuery.data.filter((comment: ResumeComment) => {
@ -91,19 +72,37 @@ export default function ResumeCommentsList({
<div className="w-fit text-lg font-medium">{label}</div> <div className="w-fit text-lg font-medium">{label}</div>
</div> </div>
{commentCount > 0 ? ( <div className="w-full space-y-4 pr-4">
comments.map((comment) => { <div
return ( className={clsx(
<ResumeCommentListItem 'space-y-2 rounded-md border-2 bg-white px-4 py-3 drop-shadow-md',
key={comment.id} commentCount ? 'border-slate-300' : 'border-slate-300',
comment={comment} )}>
userId={sessionData?.user?.id} {commentCount > 0 ? (
/> comments.map((comment) => {
); return (
}) <ResumeCommentListItem
) : ( key={comment.id}
<div>There are no comments for this section yet!</div> comment={comment}
)} userId={sessionData?.user?.id}
/>
);
})
) : (
<div className="flex flex-row items-center text-sm">
<ChatBubbleLeftRightIcon className="mr-2 h-6 w-6 text-slate-500" />
<div className="text-slate-500">
There are no comments for this section yet!
</div>
</div>
)}
</div>
</div>
<div className="relative flex flex-row pr-6 pt-2">
<div className="flex-grow border-t border-gray-300" />
</div>
</div> </div>
); );
})} })}

@ -1,40 +0,0 @@
import { useState } from 'react';
import ResumeCommentsForm from './ResumeCommentsForm';
import ResumeCommentsList from './ResumeCommentsList';
type CommentsSectionProps = {
resumeId: string;
};
export default function ResumeCommentsSection({
resumeId,
}: CommentsSectionProps) {
const [showCommentsForm, setShowCommentsForm] = useState(false);
return (
<>
<div className="relative p-2 lg:hidden">
<div aria-hidden="true" className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-slate-50 px-3 text-lg font-medium text-slate-900">
Reviews
</span>
</div>
</div>
{showCommentsForm ? (
<ResumeCommentsForm
resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm}
/>
) : (
<ResumeCommentsList
resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm}
/>
)}
</>
);
}

@ -97,7 +97,7 @@ export default function ResumeCommentVoteButtons({
/> />
</button> </button>
<div className="flex min-w-[1rem] justify-center text-xs"> <div className="flex min-w-[1rem] justify-center text-xs font-semibold text-gray-700">
{commentVotesQuery.data?.numVotes ?? 0} {commentVotesQuery.data?.numVotes ?? 0}
</div> </div>

@ -8,7 +8,7 @@ type Props = Readonly<{
export default function ResumeSignInButton({ text, className }: Props) { export default function ResumeSignInButton({ text, className }: Props) {
return ( return (
<div className={clsx('flex justify-center pt-4', className)}> <div className={clsx('flex justify-center', className)}>
<p> <p>
<a <a
className="text-indigo-500 hover:text-indigo-600" className="text-indigo-500 hover:text-indigo-600"

@ -3,7 +3,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import Error from 'next/error'; import Error from 'next/error';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react'; import { useState } from 'react';
import { import {
AcademicCapIcon, AcademicCapIcon,
@ -14,9 +14,10 @@ import {
PencilSquareIcon, PencilSquareIcon,
StarIcon, StarIcon,
} from '@heroicons/react/20/solid'; } from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui'; import { Button, Spinner } from '@tih/ui';
import ResumeCommentsSection from '~/components/resumes/comments/ResumeCommentsSection'; import ResumeCommentsForm from '~/components/resumes/comments/ResumeCommentsForm';
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
import ResumePdf from '~/components/resumes/ResumePdf'; import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText'; import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
@ -59,6 +60,7 @@ export default function ResumeReviewPage() {
session?.user?.id != null && session.user.id === detailsQuery.data?.userId; session?.user?.id != null && session.user.id === detailsQuery.data?.userId;
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [showCommentsForm, setShowCommentsForm] = useState(false);
const onStarButtonClick = () => { const onStarButtonClick = () => {
if (session?.user?.id == null) { if (session?.user?.id == null) {
@ -81,6 +83,32 @@ export default function ResumeReviewPage() {
setIsEditMode(true); setIsEditMode(true);
}; };
const renderReviewButton = () => {
if (session === null) {
return (
<div className=" flex h-10 justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-[400] hover:cursor-pointer hover:bg-slate-50">
<a
href="/api/auth/signin"
onClick={(event) => {
event.preventDefault();
signIn();
}}>
Sign in to join discussion
</a>
</div>
);
}
return (
<Button
className="h-10 py-2"
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
if (isEditMode && detailsQuery.data != null) { if (isEditMode && detailsQuery.data != null) {
return ( return (
<SubmitResumeForm <SubmitResumeForm
@ -120,10 +148,19 @@ export default function ResumeReviewPage() {
</Head> </Head>
<main className="h-[calc(100vh-2rem)] flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16"> <main className="h-[calc(100vh-2rem)] flex-1 space-y-2 overflow-y-auto py-4 px-8 xl:px-12 2xl:pr-16">
<div className="flex justify-between"> <div className="flex justify-between">
<h1 className="text-2xl font-semibold leading-7 text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight"> <h1 className="pr-2 text-2xl font-semibold leading-7 text-slate-900 sm:truncate sm:text-3xl sm:tracking-tight">
{detailsQuery.data.title} {detailsQuery.data.title}
</h1> </h1>
<div className="flex gap-4"> <div className="flex gap-4 xl:pr-4">
{userIsOwner && (
<button
className="p h-10 rounded-md border border-slate-300 bg-white py-1 px-2 text-center"
type="button"
onClick={onEditButtonClick}>
<PencilSquareIcon className="text-primary-600 hover:text-primary-300 h-6 w-6" />
</button>
)}
<button <button
className="isolate inline-flex h-10 items-center space-x-4 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 disabled:hover:bg-white" className="isolate inline-flex h-10 items-center space-x-4 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 disabled:hover:bg-white"
disabled={starMutation.isLoading || unstarMutation.isLoading} disabled={starMutation.isLoading || unstarMutation.isLoading}
@ -152,42 +189,36 @@ export default function ResumeReviewPage() {
{detailsQuery.data?._count.stars} {detailsQuery.data?._count.stars}
</span> </span>
</button> </button>
{userIsOwner && (
<button <div className="hidden xl:block">{renderReviewButton()}</div>
className="p h-10 rounded-md border border-slate-300 bg-white py-1 px-2 text-center"
type="button"
onClick={onEditButtonClick}>
<PencilSquareIcon className="text-primary-600 hover:text-primary-300 h-6 w-6" />
</button>
)}
</div> </div>
</div> </div>
<div className="flex flex-col lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8"> <div className="flex flex-col lg:mt-0 lg:flex-row lg:flex-wrap lg:space-x-8">
<div className="mt-2 flex items-center text-sm text-slate-500"> <div className="mt-2 flex items-center text-sm text-slate-600 xl:mt-1">
<BriefcaseIcon <BriefcaseIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
{detailsQuery.data.role} {detailsQuery.data.role}
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-500"> <div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<MapPinIcon <MapPinIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
{detailsQuery.data.location} {detailsQuery.data.location}
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-500"> <div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<AcademicCapIcon <AcademicCapIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
{detailsQuery.data.experience} {detailsQuery.data.experience}
</div> </div>
<div className="flex items-center pt-2 text-sm text-slate-500"> <div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
<CalendarIcon <CalendarIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
{`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, { {`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, {
addSuffix: true, addSuffix: true,
@ -195,10 +226,10 @@ export default function ResumeReviewPage() {
</div> </div>
</div> </div>
{detailsQuery.data.additionalInfo && ( {detailsQuery.data.additionalInfo && (
<div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-slate-500"> <div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-slate-600 xl:pt-1">
<InformationCircleIcon <InformationCircleIcon
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400" className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
/> />
<ResumeExpandableText <ResumeExpandableText
key={detailsQuery.data.additionalInfo} key={detailsQuery.data.additionalInfo}
@ -206,12 +237,35 @@ export default function ResumeReviewPage() {
/> />
</div> </div>
)} )}
<div className="flex w-full flex-col gap-6 py-4 lg:flex-row">
<div className="w-full lg:w-[780px]"> <div className="flex w-full flex-col gap-6 py-4 xl:flex-row xl:py-0">
<div className="w-full xl:w-1/2">
<ResumePdf url={detailsQuery.data.url} /> <ResumePdf url={detailsQuery.data.url} />
</div> </div>
<div className="grow"> <div className="grow">
<ResumeCommentsSection resumeId={resumeId as string} /> <div className="relative p-2 xl:hidden">
<div
aria-hidden="true"
className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-300" />
</div>
<div className="relative flex justify-center">
<span className="bg-slate-50 px-3 text-lg font-medium text-slate-900">
Reviews
</span>
</div>
</div>
<div className="mb-4 xl:hidden">{renderReviewButton()}</div>
{showCommentsForm ? (
<ResumeCommentsForm
resumeId={resumeId as string}
setShowCommentsForm={setShowCommentsForm}
/>
) : (
<ResumeCommentsList resumeId={resumeId as string} />
)}
</div> </div>
</div> </div>
</main> </main>

Loading…
Cancel
Save