mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-03-30 17:10:16 +08:00
[portal][refactor] make comments appearance more consistent across offers and resume reviews
This commit is contained in:
@@ -186,7 +186,7 @@ export default function ProfileComments({
|
||||
display="block"
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Comment"
|
||||
label="Submit"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => handleComment(currentReply)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import { useState } from 'react';
|
||||
import { Button, Dialog, TextArea, useToast } from '@tih/ui';
|
||||
|
||||
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
|
||||
|
||||
import { timeSinceNow } from '~/utils/offers/time';
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import type { Reply } from '~/types/offers';
|
||||
@@ -135,45 +135,45 @@ export default function CommentCard({
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-slate-900">
|
||||
{user?.name ?? 'unknown user'}
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{user?.name ?? 'Unknown user'}
|
||||
</p>
|
||||
<span className="font-medium text-slate-500">·</span>
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatDistanceToNow(createdAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-slate-700">
|
||||
<p className="break-all">{message}</p>
|
||||
</div>
|
||||
<div className="mt-2 space-x-2 text-xs">
|
||||
<span className="font-medium text-slate-500">
|
||||
{timeSinceNow(createdAt)} ago
|
||||
</span>{' '}
|
||||
{replyLength > 0 && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="font-medium text-slate-900"
|
||||
type="button"
|
||||
onClick={handleExpanded}>
|
||||
{isExpanded ? `Hide replies` : `View replies (${replyLength})`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<div className="-ml-2 mt-1 flex h-6 items-center text-xs">
|
||||
{!disableReply && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="font-medium text-slate-900"
|
||||
type="button"
|
||||
onClick={() => setIsReplying(!isReplying)}>
|
||||
Reply
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setIsReplying(!isReplying)}>
|
||||
Reply
|
||||
</button>
|
||||
)}
|
||||
{replyLength > 0 && (
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={handleExpanded}>
|
||||
{isExpanded
|
||||
? `Hide ${replyLength === 1 ? 'reply' : 'replies'}`
|
||||
: `Show ${replyLength} ${
|
||||
replyLength === 1 ? 'reply' : 'replies'
|
||||
}`}
|
||||
</button>
|
||||
)}
|
||||
{deletable && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="font-medium text-slate-900"
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
disabled={deleteCommentMutation.isLoading}
|
||||
type="button"
|
||||
onClick={() => setIsDialogOpen(true)}>
|
||||
@@ -210,8 +210,9 @@ export default function CommentCard({
|
||||
)}
|
||||
</div>
|
||||
{!disableReply && isReplying && (
|
||||
<div className="mt-4 mr-2">
|
||||
<div className="mt-2">
|
||||
<form
|
||||
className="space-y-2"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -226,23 +227,29 @@ export default function CommentCard({
|
||||
value={currentReply}
|
||||
onChange={(value) => setCurrentReply(value)}
|
||||
/>
|
||||
<div className="mt-2 flex w-full justify-end">
|
||||
<div className="w-fit">
|
||||
<Button
|
||||
disabled={
|
||||
!currentReply.length ||
|
||||
createCommentMutation.isLoading ||
|
||||
deleteCommentMutation.isLoading
|
||||
}
|
||||
display="block"
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Reply"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={handleReply}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
<Button
|
||||
disabled={createCommentMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setIsReplying(false);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
!currentReply.length ||
|
||||
createCommentMutation.isLoading ||
|
||||
deleteCommentMutation.isLoading
|
||||
}
|
||||
isLabelHidden={false}
|
||||
isLoading={createCommentMutation.isLoading}
|
||||
label="Submit"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={handleReply}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -27,19 +27,21 @@ export default function ExpandableCommentCard({
|
||||
token={token}
|
||||
/>
|
||||
{comment.replies && comment.replies.length > 0 && isExpanded && (
|
||||
<div className="pt-4">
|
||||
<ul className="space-y-4 pl-14" role="list">
|
||||
{comment.replies.map((reply) => (
|
||||
<li key={reply.id}>
|
||||
<CommentCard
|
||||
comment={reply}
|
||||
disableReply={true}
|
||||
profileId={profileId}
|
||||
token={token}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="pl-[52px] pt-2">
|
||||
<div className="border-l-2 border-slate-200 pl-2">
|
||||
<ul className="space-y-2" role="list">
|
||||
{comment.replies.map((reply) => (
|
||||
<li key={reply.id}>
|
||||
<CommentCard
|
||||
comment={reply}
|
||||
disableReply={true}
|
||||
profileId={profileId}
|
||||
token={token}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function ResumeCommentListItem({
|
||||
<div className="flex w-full flex-col space-y-1">
|
||||
{/* Name and creation time */}
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<p className={clsx('text-sm font-medium text-slate-800')}>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{comment.user.name ?? 'Reviewer ABC'}
|
||||
</p>
|
||||
{isCommentOwner && (
|
||||
@@ -83,64 +83,59 @@ export default function ResumeCommentListItem({
|
||||
)}
|
||||
|
||||
{/* Upvote and edit */}
|
||||
<div className="flex flex-row space-x-2 pt-1 align-middle">
|
||||
<div className="mt-1 flex h-6 items-center">
|
||||
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
|
||||
{/* Action buttons; only present for authenticated user when not editing/replying */}
|
||||
{userId && !isEditingComment && !isReplyingComment && (
|
||||
<>
|
||||
{isCommentOwner && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="text-xs font-medium text-slate-500 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setIsEditingComment(true)}>
|
||||
Edit
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!comment.parentId && (
|
||||
<>
|
||||
<span className="font-medium text-slate-500">·</span>{' '}
|
||||
<button
|
||||
className="text-xs font-medium text-slate-500 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => setIsReplyingComment(true)}>
|
||||
Reply
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{userId && !comment.parentId && (
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsReplyingComment(!isReplyingComment);
|
||||
setIsEditingComment(false);
|
||||
}}>
|
||||
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>
|
||||
</>
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 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>
|
||||
)}
|
||||
{isCommentOwner && (
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsEditingComment(!isEditingComment);
|
||||
setIsReplyingComment(false);
|
||||
}}>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reply Form */}
|
||||
{isReplyingComment && (
|
||||
<ResumeCommentReplyForm
|
||||
parentId={comment.id}
|
||||
resumeId={comment.resumeId}
|
||||
section={comment.section}
|
||||
setIsReplyingComment={setIsReplyingComment}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<ResumeCommentReplyForm
|
||||
parentId={comment.id}
|
||||
resumeId={comment.resumeId}
|
||||
section={comment.section}
|
||||
setIsReplyingComment={setIsReplyingComment}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Replies */}
|
||||
|
||||
@@ -67,39 +67,35 @@ export default function ResumeCommentEditForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex-column mt-1 space-y-2">
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Comments cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue={comment.description}
|
||||
<form className="space-y-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Comments cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue={comment.description}
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your comment here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
<Button
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your comment here"
|
||||
onChange={setFormValue}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<Button
|
||||
disabled={!isDirty || commentUpdateMutation.isLoading}
|
||||
isLoading={commentUpdateMutation.isLoading}
|
||||
label="Submit"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentUpdateMutation.isLoading}
|
||||
isLoading={commentUpdateMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -83,15 +83,16 @@ export default function ResumeCommentReplyForm({
|
||||
required: 'Reply cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
autoFocus={true}
|
||||
defaultValue=""
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label=""
|
||||
placeholder="Leave your reply here"
|
||||
isLabelHidden={true}
|
||||
label="Reply to comment"
|
||||
placeholder="Type your reply here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
<Button
|
||||
disabled={commentReplyMutation.isLoading}
|
||||
label="Cancel"
|
||||
@@ -99,11 +100,10 @@ export default function ResumeCommentReplyForm({
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentReplyMutation.isLoading}
|
||||
isLoading={commentReplyMutation.isLoading}
|
||||
label="Confirm"
|
||||
label="Submit"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
|
||||
@@ -2,40 +2,6 @@ import { getMonth, getYear } from 'date-fns';
|
||||
|
||||
import type { MonthYear } from '~/components/shared/MonthYearPicker';
|
||||
|
||||
export function timeSinceNow(date: Date | number | string) {
|
||||
const seconds = Math.floor(
|
||||
new Date().getTime() / 1000 - new Date(date).getTime() / 1000,
|
||||
);
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} year` : `${time} years`;
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} month` : `${time} months`;
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} day` : `${time} days`;
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} hour` : `${time} hours`;
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} minute` : `${time} minutes`;
|
||||
}
|
||||
const time: number = Math.floor(interval);
|
||||
return time === 1 ? `${time} second` : `${time} seconds`;
|
||||
}
|
||||
|
||||
export function formatDate(value: Date | number | string) {
|
||||
const date = new Date(value);
|
||||
const month = date.toLocaleString('default', { month: 'short' });
|
||||
|
||||
Reference in New Issue
Block a user