mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-06 17:10:58 +08:00
[resumes][refactor] tweak resume review page UI
This commit is contained in:
@ -194,7 +194,7 @@ export default function AppShell({ children }: Props) {
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3BottomLeftIcon aria-hidden="true" className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="flex flex-1 justify-between px-4 sm:px-6">
|
||||
<div className="flex flex-1 justify-between px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-1 items-center">
|
||||
<ProductNavigation
|
||||
items={currentProductNavigation.navigation}
|
||||
|
@ -36,9 +36,9 @@ export default function ResumePdf({ url }: Props) {
|
||||
|
||||
return (
|
||||
<div className="w-full" id="pdfView">
|
||||
<div className="group relative">
|
||||
<div className="group relative bg-slate-100">
|
||||
<Document
|
||||
className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto"
|
||||
className="flex h-[calc(100vh-16rem)] flex-row justify-center overflow-auto py-8"
|
||||
file={url}
|
||||
loading={<Spinner display="block" size="lg" />}
|
||||
noData=""
|
||||
@ -79,7 +79,7 @@ export default function ResumePdf({ url }: Props) {
|
||||
</div>
|
||||
</Document>
|
||||
</div>
|
||||
<div className="flex justify-center p-4">
|
||||
<div className="flex justify-center border-t border-slate-200 bg-white py-4">
|
||||
<Pagination
|
||||
current={pageNumber}
|
||||
end={numPages}
|
||||
|
@ -39,18 +39,22 @@ export default function ResumeUserBadges({ userId }: Props) {
|
||||
topUpvotedCommentCount: userTopUpvotedCommentCountQuery.data ?? 0,
|
||||
};
|
||||
|
||||
const badges = RESUME_USER_BADGES.filter((badge) => badge.isValid(payload));
|
||||
|
||||
if (badges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{RESUME_USER_BADGES.filter((badge) => badge.isValid(payload)).map(
|
||||
(badge) => (
|
||||
<ResumeUserBadge
|
||||
key={badge.id}
|
||||
description={badge.description}
|
||||
icon={badge.icon}
|
||||
title={badge.title}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{badges.map((badge) => (
|
||||
<ResumeUserBadge
|
||||
key={badge.id}
|
||||
description={badge.description}
|
||||
icon={badge.icon}
|
||||
title={badge.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
import { ChevronUpIcon } from '@heroicons/react/20/solid';
|
||||
import { FaceSmileIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
|
||||
@ -12,10 +11,10 @@ import ResumeExpandableText from '../shared/ResumeExpandableText';
|
||||
|
||||
import type { ResumeComment } from '~/types/resume-comments';
|
||||
|
||||
type ResumeCommentListItemProps = {
|
||||
type ResumeCommentListItemProps = Readonly<{
|
||||
comment: ResumeComment;
|
||||
userId: string | undefined;
|
||||
};
|
||||
}>;
|
||||
|
||||
export default function ResumeCommentListItem({
|
||||
comment,
|
||||
@ -28,14 +27,14 @@ export default function ResumeCommentListItem({
|
||||
|
||||
return (
|
||||
<div className="min-w-fit">
|
||||
<div className="flex flex-row space-x-2 p-1 align-top">
|
||||
<div className="flex flex-row space-x-3 align-top">
|
||||
{/* Image Icon */}
|
||||
{comment.user.image ? (
|
||||
<img
|
||||
alt={comment.user.name ?? 'Reviewer'}
|
||||
className={clsx(
|
||||
'mt-1 rounded-full',
|
||||
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
|
||||
comment.parentId ? 'h-8 w-8' : 'h-10 w-10',
|
||||
)}
|
||||
src={comment.user.image!}
|
||||
/>
|
||||
@ -50,24 +49,18 @@ export default function ResumeCommentListItem({
|
||||
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
{/* Name and creation time */}
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<p
|
||||
className={clsx(
|
||||
'font-medium text-gray-800',
|
||||
!!comment.parentId && 'text-sm',
|
||||
)}>
|
||||
{comment.user.name ?? 'Reviewer ABC'}
|
||||
</p>
|
||||
|
||||
<p className="text-primary-800 text-xs font-medium">
|
||||
{isCommentOwner ? '(Me)' : ''}
|
||||
</p>
|
||||
|
||||
<ResumeUserBadges userId={comment.user.userId} />
|
||||
</div>
|
||||
|
||||
<div className="px-2 text-xs text-slate-600">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<p className={clsx('text-sm font-medium text-slate-800')}>
|
||||
{comment.user.name ?? 'Reviewer ABC'}
|
||||
</p>
|
||||
{isCommentOwner && (
|
||||
<span className="bg-primary-100 text-primary-800 rounded-md py-0.5 px-1 text-xs">
|
||||
Me
|
||||
</span>
|
||||
)}
|
||||
<ResumeUserBadges userId={comment.user.userId} />
|
||||
<span className="font-medium text-slate-500">·</span>
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatDistanceToNow(comment.createdAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
@ -81,7 +74,7 @@ export default function ResumeCommentListItem({
|
||||
setIsEditingComment={setIsEditingComment}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-800">
|
||||
<div className="text-slate-800">
|
||||
<ResumeExpandableText
|
||||
key={comment.description}
|
||||
text={comment.description}
|
||||
@ -90,31 +83,54 @@ export default function ResumeCommentListItem({
|
||||
)}
|
||||
|
||||
{/* Upvote and edit */}
|
||||
<div className="flex flex-row space-x-1 pt-1 align-middle">
|
||||
<div className="flex flex-row space-x-2 pt-1 align-middle">
|
||||
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
|
||||
|
||||
{/* Action buttons; only present for authenticated user when not editing/replying */}
|
||||
{userId && !isEditingComment && !isReplyingComment && (
|
||||
<>
|
||||
{isCommentOwner && (
|
||||
<button
|
||||
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
|
||||
type="button"
|
||||
onClick={() => setIsEditingComment(true)}>
|
||||
Edit
|
||||
</button>
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="px-1 text-xs font-medium text-slate-500 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setIsEditingComment(true)}>
|
||||
Edit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!comment.parentId && (
|
||||
<button
|
||||
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
|
||||
type="button"
|
||||
onClick={() => setIsReplyingComment(true)}>
|
||||
Reply
|
||||
</button>
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="px-1 text-xs font-medium text-slate-500 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setIsReplyingComment(true)}>
|
||||
Reply
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{comment.children.length > 0 && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="flex items-center space-x-1 rounded-md text-xs font-medium text-slate-500 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setShowReplies(!showReplies)}>
|
||||
<span>
|
||||
{showReplies
|
||||
? `Hide ${
|
||||
comment.children.length === 1 ? 'reply' : 'replies'
|
||||
}`
|
||||
: `Show ${comment.children.length} ${
|
||||
comment.children.length === 1 ? 'reply' : 'replies'
|
||||
}`}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reply Form */}
|
||||
@ -128,48 +144,21 @@ export default function ResumeCommentListItem({
|
||||
)}
|
||||
|
||||
{/* Replies */}
|
||||
{comment.children.length > 0 && (
|
||||
{comment.children.length > 0 && showReplies && (
|
||||
<div className="min-w-fit space-y-1 pt-2">
|
||||
<button
|
||||
className="text-primary-800 hover:text-primary-300 flex items-center space-x-1 rounded-md text-xs font-medium"
|
||||
type="button"
|
||||
onClick={() => setShowReplies(!showReplies)}>
|
||||
<ChevronUpIcon
|
||||
className={clsx(
|
||||
'h-5 w-5 ',
|
||||
!showReplies && 'rotate-180 transform',
|
||||
)}
|
||||
/>
|
||||
<span>
|
||||
{showReplies
|
||||
? `Hide ${
|
||||
comment.children.length === 1 ? 'reply' : 'replies'
|
||||
}`
|
||||
: `Show ${comment.children.length} ${
|
||||
comment.children.length === 1 ? 'reply' : 'replies'
|
||||
}`}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showReplies && (
|
||||
<div className="flex flex-row">
|
||||
<div className="relative flex flex-col px-2 py-2">
|
||||
<div className="flex-grow border-r border-slate-300" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col space-y-1">
|
||||
{comment.children.map((child) => {
|
||||
return (
|
||||
<ResumeCommentListItem
|
||||
key={child.id}
|
||||
comment={child}
|
||||
userId={userId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-row border-l-2 border-slate-200 pl-2">
|
||||
<div className="flex flex-1 flex-col space-y-1">
|
||||
{comment.children.map((child) => {
|
||||
return (
|
||||
<ResumeCommentListItem
|
||||
key={child.id}
|
||||
comment={child}
|
||||
userId={userId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -115,10 +115,12 @@ export default function ResumeCommentsForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-13rem)] overflow-y-auto pb-4">
|
||||
<h2 className="text-xl font-semibold text-slate-800">Add your review</h2>
|
||||
<p className="text-slate-800">
|
||||
Please fill in at least one section to submit your review
|
||||
<div className="overflow-y-auto py-8 px-4 lg:h-[calc(100vh-13rem)]">
|
||||
<h2 className="text-xl font-medium text-slate-800">
|
||||
Contribute a review
|
||||
</h2>
|
||||
<p className="mt-1 text-slate-600">
|
||||
Please fill in at least one section to submit a review.
|
||||
</p>
|
||||
|
||||
<form
|
||||
|
@ -1,9 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import {
|
||||
BookOpenIcon,
|
||||
BriefcaseIcon,
|
||||
ChatBubbleLeftRightIcon,
|
||||
CodeBracketSquareIcon,
|
||||
FaceSmileIcon,
|
||||
IdentificationIcon,
|
||||
@ -31,7 +29,7 @@ export default function ResumeCommentsList({
|
||||
const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }]);
|
||||
|
||||
const renderIcon = (section: ResumesSection) => {
|
||||
const className = 'h-7 w-7';
|
||||
const className = 'h-5 w-5';
|
||||
switch (section) {
|
||||
case ResumesSection.GENERAL:
|
||||
return <IdentificationIcon className={className} />;
|
||||
@ -57,7 +55,7 @@ export default function ResumeCommentsList({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flow-root w-full flex-col space-y-10 overflow-y-auto overflow-x-hidden lg:h-[calc(100vh-13rem)]">
|
||||
<div className="flow-root w-full space-y-4 overflow-y-auto overflow-x-hidden px-4 lg:h-[calc(100vh-12rem)] lg:py-8">
|
||||
{RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
|
||||
const comments = commentsQuery.data
|
||||
? commentsQuery.data.filter((comment: ResumeComment) => {
|
||||
@ -67,22 +65,19 @@ export default function ResumeCommentsList({
|
||||
const commentCount = comments.length;
|
||||
|
||||
return (
|
||||
<div key={value} className="space-y-4 pr-4">
|
||||
<div
|
||||
key={value}
|
||||
className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
{/* CommentHeader Section */}
|
||||
<div className="text-primary-800 flex items-center space-x-2">
|
||||
<hr className="flex-grow border-slate-800" />
|
||||
<div className="flex items-center space-x-2 border-b border-slate-200 px-4 py-3 font-medium text-slate-700">
|
||||
{renderIcon(value)}
|
||||
|
||||
<span className="w-fit text-lg font-medium">{label}</span>
|
||||
<hr className="flex-grow border-slate-800" />
|
||||
<span className="w-fit text-sm font-medium uppercase tracking-wide">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Comment Section */}
|
||||
<div
|
||||
className={clsx(
|
||||
'space-y-2 rounded-md border-2 bg-white px-4 py-3 drop-shadow-md',
|
||||
commentCount ? 'border-slate-300' : 'border-slate-300',
|
||||
)}>
|
||||
<div className="space-y-4 px-4 py-3">
|
||||
{commentCount > 0 ? (
|
||||
comments.map((comment) => {
|
||||
return (
|
||||
@ -95,10 +90,8 @@ export default function ResumeCommentsList({
|
||||
})
|
||||
) : (
|
||||
<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!
|
||||
There are no comments for this section.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -77,39 +77,37 @@ export default function ResumeCommentReplyForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex-column space-y-2 pt-2">
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Reply cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue=""
|
||||
<form className="space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Reply cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue=""
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your reply here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your reply here"
|
||||
onChange={setFormValue}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentReplyMutation.isLoading}
|
||||
isLoading={commentReplyMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!isDirty || commentReplyMutation.isLoading}
|
||||
isLoading={commentReplyMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
@ -1,10 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid';
|
||||
import { Vote } from '@prisma/client';
|
||||
|
||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||
@ -92,8 +89,9 @@ export default function ResumeCommentVoteButtons({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="-m-1 rounded-full p-1 hover:bg-slate-100"
|
||||
disabled={
|
||||
commentVotesQuery.isLoading ||
|
||||
commentVotesUpsertMutation.isLoading ||
|
||||
@ -101,27 +99,36 @@ export default function ResumeCommentVoteButtons({
|
||||
}
|
||||
type="button"
|
||||
onClick={() => onVote(Vote.UPVOTE, setUpvoteAnimation)}>
|
||||
<ArrowUpCircleIcon
|
||||
<ChevronUpIcon
|
||||
className={clsx(
|
||||
'h-4 w-4',
|
||||
'h-5 w-5',
|
||||
commentVotesQuery.data?.userVote?.value === Vote.UPVOTE ||
|
||||
upvoteAnimation
|
||||
? 'fill-primary-500'
|
||||
: 'fill-slate-400',
|
||||
? 'text-primary-500'
|
||||
: 'text-slate-400',
|
||||
userId &&
|
||||
!downvoteAnimation &&
|
||||
!upvoteAnimation &&
|
||||
'hover:fill-primary-500',
|
||||
'hover:text-primary-500',
|
||||
upvoteAnimation && 'animate-[bounce_0.5s_infinite] cursor-default',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="flex min-w-[1rem] justify-center text-xs font-semibold text-gray-700">
|
||||
<div className="mx-1 flex min-w-[1rem] justify-center text-xs font-semibold text-gray-700">
|
||||
{commentVotesQuery.data?.numVotes ?? 0}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={clsx(
|
||||
'-m-1 rounded-full p-1 hover:bg-slate-100',
|
||||
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
|
||||
downvoteAnimation
|
||||
? 'text-danger-500'
|
||||
: 'text-slate-400',
|
||||
userId &&
|
||||
!downvoteAnimation &&
|
||||
!upvoteAnimation &&
|
||||
'hover:text-danger-500',
|
||||
)}
|
||||
disabled={
|
||||
commentVotesQuery.isLoading ||
|
||||
commentVotesUpsertMutation.isLoading ||
|
||||
@ -129,22 +136,14 @@ export default function ResumeCommentVoteButtons({
|
||||
}
|
||||
type="button"
|
||||
onClick={() => onVote(Vote.DOWNVOTE, setDownvoteAnimation)}>
|
||||
<ArrowDownCircleIcon
|
||||
<ChevronDownIcon
|
||||
className={clsx(
|
||||
'h-4 w-4',
|
||||
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
|
||||
downvoteAnimation
|
||||
? 'fill-danger-500'
|
||||
: 'fill-slate-400',
|
||||
userId &&
|
||||
!downvoteAnimation &&
|
||||
!upvoteAnimation &&
|
||||
'hover:fill-danger-500',
|
||||
'h-5 w-5',
|
||||
downvoteAnimation &&
|
||||
'animate-[bounce_0.5s_infinite] cursor-default',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -29,14 +29,14 @@ export default function ResumeExpandableText({
|
||||
<span
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'line-clamp-3 whitespace-pre-wrap text-sm',
|
||||
'line-clamp-3 whitespace-pre-wrap text-xs sm:text-sm',
|
||||
isExpanded ? 'line-clamp-none' : '',
|
||||
)}>
|
||||
{text}
|
||||
</span>
|
||||
{descriptionOverflow && (
|
||||
<p
|
||||
className="text-primary-500 hover:text-primary-300 mt-1 cursor-pointer text-xs"
|
||||
className="text-primary-500 hover:text-primary-300 mt-1 cursor-pointer text-xs sm:text-sm"
|
||||
onClick={onSeeActionClicked}>
|
||||
{isExpanded ? 'See Less' : 'See More'}
|
||||
</p>
|
||||
|
@ -180,10 +180,9 @@ export default function ResumeReviewPage() {
|
||||
};
|
||||
|
||||
const renderReviewButton = () => {
|
||||
if (session === null) {
|
||||
if (session == null) {
|
||||
return (
|
||||
<Button
|
||||
className="h-10 shadow-md"
|
||||
display="block"
|
||||
href={loginPageHref()}
|
||||
label="Log in to join discussion"
|
||||
@ -191,9 +190,9 @@ export default function ResumeReviewPage() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="h-10 shadow-md"
|
||||
display="block"
|
||||
label="Add your review"
|
||||
variant="primary"
|
||||
@ -224,182 +223,192 @@ export default function ResumeReviewPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(detailsQuery.isError || detailsQuery.data === null) && ErrorPage}
|
||||
{(detailsQuery.isError || detailsQuery.data == null) && ErrorPage}
|
||||
{detailsQuery.isLoading && (
|
||||
<div className="w-full pt-4">
|
||||
{' '}
|
||||
<Spinner display="block" size="lg" />{' '}
|
||||
<Spinner display="block" size="lg" />
|
||||
</div>
|
||||
)}
|
||||
{detailsQuery.isFetched && detailsQuery.data && (
|
||||
<>
|
||||
<Head>
|
||||
<title>{detailsQuery.data.title}</title>
|
||||
<title>{`${detailsQuery.data.title} | Resume Review`}</title>
|
||||
</Head>
|
||||
<main className="h-full flex-1 space-y-2 py-4 px-8 xl:px-12 2xl:pr-16">
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<h1 className="w-[60%] pr-2 text-2xl font-semibold leading-7 text-slate-900">
|
||||
{detailsQuery.data.title}
|
||||
</h1>
|
||||
<div className="flex gap-3 xl:pr-4">
|
||||
{userIsOwner && (
|
||||
<>
|
||||
<Button
|
||||
addonPosition="start"
|
||||
className="h-10 shadow-md"
|
||||
icon={PencilSquareIcon}
|
||||
label="Edit"
|
||||
variant="tertiary"
|
||||
onClick={onEditButtonClick}
|
||||
<main className="h-full w-full bg-white">
|
||||
<div className="mx-auto space-y-4 border-b border-slate-200 px-4 py-6 sm:px-6 md:space-y-2 lg:px-8">
|
||||
<div className="flex flex-wrap justify-between space-y-4 lg:space-y-0">
|
||||
<h1 className="pr-2 text-xl font-medium leading-7 text-slate-900 sm:text-2xl lg:w-[60%]">
|
||||
{detailsQuery.data.title}
|
||||
</h1>
|
||||
<div className="flex gap-3">
|
||||
{userIsOwner && (
|
||||
<>
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={PencilSquareIcon}
|
||||
label="Edit"
|
||||
variant="tertiary"
|
||||
onClick={onEditButtonClick}
|
||||
/>
|
||||
<button
|
||||
className="isolate inline-flex items-center space-x-4 rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:ring-slate-600 disabled:hover:bg-white"
|
||||
disabled={resolveMutation.isLoading}
|
||||
type="button"
|
||||
onClick={onResolveButtonClick}>
|
||||
<div className="-ml-1 mr-2 h-5 w-5">
|
||||
{resolveMutation.isLoading ? (
|
||||
<Spinner className="mt-0.5" size="xs" />
|
||||
) : (
|
||||
<CheckCircleIcon
|
||||
aria-hidden="true"
|
||||
className={
|
||||
isResumeResolved
|
||||
? 'text-slate-500'
|
||||
: 'text-success-600'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isResumeResolved
|
||||
? 'Reopen for review'
|
||||
: 'Mark as reviewed'}
|
||||
</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 hover:bg-slate-50 focus:ring-slate-600 disabled:hover:bg-white"
|
||||
disabled={
|
||||
starMutation.isLoading || unstarMutation.isLoading
|
||||
}
|
||||
type="button"
|
||||
onClick={onStarButtonClick}>
|
||||
<div className="-ml-1 mr-2 h-5 w-5">
|
||||
{starMutation.isLoading ||
|
||||
unstarMutation.isLoading ||
|
||||
detailsQuery.isLoading ? (
|
||||
<Spinner className="mt-0.5" size="xs" />
|
||||
) : (
|
||||
<StarIcon
|
||||
aria-hidden="true"
|
||||
className={clsx(
|
||||
detailsQuery.data?.stars.length
|
||||
? 'text-orange-400'
|
||||
: 'text-slate-400',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{detailsQuery.data?.stars.length ? 'Starred' : 'Star'}
|
||||
<span className="relative -ml-px inline-flex">
|
||||
{detailsQuery.data?._count.stars}
|
||||
</span>
|
||||
</button>
|
||||
<div className="hidden xl:block">{renderReviewButton()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2 md:flex md:flex-wrap md:space-x-8">
|
||||
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
|
||||
<BriefcaseIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<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-md hover:bg-slate-50 focus:ring-slate-600 disabled:hover:bg-white"
|
||||
disabled={resolveMutation.isLoading}
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={onResolveButtonClick}>
|
||||
<div className="-ml-1 mr-2 h-5 w-5">
|
||||
{resolveMutation.isLoading ? (
|
||||
<Spinner className="mt-0.5" size="xs" />
|
||||
) : (
|
||||
<CheckCircleIcon
|
||||
aria-hidden="true"
|
||||
className={
|
||||
isResumeResolved
|
||||
? 'text-slate-500'
|
||||
: 'text-success-600'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isResumeResolved
|
||||
? 'Reopen for review'
|
||||
: 'Mark as reviewed'}
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
roleLabel: detailsQuery.data?.role,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(
|
||||
ROLES,
|
||||
detailsQuery.data.role as RoleFilter,
|
||||
)}
|
||||
</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-md hover:bg-slate-50 focus:ring-slate-600 disabled:hover:bg-white"
|
||||
disabled={starMutation.isLoading || unstarMutation.isLoading}
|
||||
type="button"
|
||||
onClick={onStarButtonClick}>
|
||||
<div className="-ml-1 mr-2 h-5 w-5">
|
||||
{starMutation.isLoading ||
|
||||
unstarMutation.isLoading ||
|
||||
detailsQuery.isLoading ? (
|
||||
<Spinner className="mt-0.5" size="xs" />
|
||||
) : (
|
||||
<StarIcon
|
||||
aria-hidden="true"
|
||||
className={clsx(
|
||||
detailsQuery.data?.stars.length
|
||||
? 'text-orange-400'
|
||||
: 'text-slate-400',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{detailsQuery.data?.stars.length ? 'Starred' : 'Star'}
|
||||
<span className="relative -ml-px inline-flex">
|
||||
{detailsQuery.data?._count.stars}
|
||||
</span>
|
||||
</button>
|
||||
<div className="hidden xl:block">{renderReviewButton()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-600 xl:mt-1">
|
||||
<BriefcaseIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<button
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
roleLabel: detailsQuery.data?.role,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(ROLES, detailsQuery.data.role as RoleFilter)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
<MapPinIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<button
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
locationLabel: detailsQuery.data?.location,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(
|
||||
LOCATIONS,
|
||||
detailsQuery.data.location as LocationFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
<AcademicCapIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<button
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
experienceLabel: detailsQuery.data?.experience,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(
|
||||
EXPERIENCES,
|
||||
detailsQuery.data.experience as ExperienceFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
<CalendarIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
{`Uploaded ${formatDistanceToNow(detailsQuery.data.createdAt, {
|
||||
addSuffix: true,
|
||||
})} by ${detailsQuery.data.user.name}`}
|
||||
</div>
|
||||
</div>
|
||||
{detailsQuery.data.additionalInfo && (
|
||||
<div className="flex items-start whitespace-pre-wrap pt-2 text-sm text-slate-600 xl:pt-1">
|
||||
<InformationCircleIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<ResumeExpandableText
|
||||
key={detailsQuery.data.additionalInfo}
|
||||
text={detailsQuery.data.additionalInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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} />
|
||||
</div>
|
||||
<div className="grow">
|
||||
<div className="mb-6 space-y-4 xl:hidden">
|
||||
{renderReviewButton()}
|
||||
<div className="flex items-center space-x-2">
|
||||
<hr className="flex-grow border-slate-300" />
|
||||
<span className="bg-slate-50 px-3 text-lg font-medium text-slate-900">
|
||||
Reviews
|
||||
</span>
|
||||
<hr className="flex-grow border-slate-300" />
|
||||
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
|
||||
<MapPinIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<button
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
locationLabel: detailsQuery.data?.location,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(
|
||||
LOCATIONS,
|
||||
detailsQuery.data.location as LocationFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
|
||||
<AcademicCapIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<button
|
||||
className="hover:text-primary-800 underline"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onInfoTagClick({
|
||||
experienceLabel: detailsQuery.data?.experience,
|
||||
})
|
||||
}>
|
||||
{getFilterLabel(
|
||||
EXPERIENCES,
|
||||
detailsQuery.data.experience as ExperienceFilter,
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center text-xs text-slate-600 sm:text-sm">
|
||||
<CalendarIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
{`Uploaded ${formatDistanceToNow(
|
||||
detailsQuery.data.createdAt,
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
)} by ${detailsQuery.data.user.name}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detailsQuery.data.additionalInfo && (
|
||||
<div className="col-span-2 flex items-start whitespace-pre-wrap pt-2 text-slate-600 xl:pt-1">
|
||||
<InformationCircleIcon
|
||||
aria-hidden="true"
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-indigo-400"
|
||||
/>
|
||||
<ResumeExpandableText
|
||||
key={detailsQuery.data.additionalInfo}
|
||||
text={detailsQuery.data.additionalInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col divide-x divide-slate-200 py-4 lg:flex-row xl:py-0">
|
||||
<div className="w-full bg-slate-100 lg:w-1/2">
|
||||
<ResumePdf url={detailsQuery.data.url} />
|
||||
</div>
|
||||
<div className="grow border-t border-slate-200 bg-slate-50 lg:border-none">
|
||||
<div className="divide-y divide-slate-200 lg:hidden">
|
||||
<div className="bg-white p-4 lg:p-0">
|
||||
{renderReviewButton()}
|
||||
</div>
|
||||
{!showCommentsForm && (
|
||||
<div className="p-4 lg:p-0">
|
||||
<h2 className="text-xl font-medium text-slate-900">
|
||||
Reviews
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showCommentsForm ? (
|
||||
<ResumeCommentsForm
|
||||
resumeId={resumeId as string}
|
||||
|
Reference in New Issue
Block a user