[offers][feat] offer discussion section

pull/392/head
Zhang Ziqing 3 years ago
parent dc818c90be
commit 86029ff266

@ -1,30 +1,102 @@
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline'; import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline';
import { Button, HorizontalDivider, Spinner, TextArea } from '@tih/ui'; import { Button, HorizontalDivider, Spinner, TextArea } from '@tih/ui';
import ExpandableCommentCard from './comments/ExpandableCommentCard'; import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
import { trpc } from '~/utils/trpc';
import type { OffersDiscussion, Reply } from '~/types/offers';
type ProfileHeaderProps = Readonly<{ type ProfileHeaderProps = Readonly<{
handleCopyEditLink: () => void;
handleCopyPublicLink: () => void;
isDisabled: boolean; isDisabled: boolean;
isEditable: boolean; isEditable: boolean;
isLoading: boolean; isLoading: boolean;
profileId: string;
profileName?: string;
token?: string;
}>; }>;
export default function ProfileComments({ export default function ProfileComments({
handleCopyEditLink,
handleCopyPublicLink,
isDisabled, isDisabled,
isEditable, isEditable,
isLoading, isLoading,
profileId,
profileName,
token,
}: ProfileHeaderProps) { }: ProfileHeaderProps) {
function handleReply(replayingToId: string, userId: string) { const { data: session, status } = useSession();
return replayingToId + userId; // To integrate with API const [currentReply, setCurrentReply] = useState<string>('');
const [replies, setReplies] = useState<Array<Reply>>();
const commentsQuery = trpc.useQuery(
['offers.comments.getComments', { profileId }],
{
onSuccess(response: OffersDiscussion) {
setReplies(response.data);
},
},
);
const trpcContext = trpc.useContext();
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
onSuccess() {
trpcContext.invalidateQueries([
'offers.comments.getComments',
{ profileId },
]);
},
});
function handleComment(message: string) {
if (isEditable) {
// If it is with edit permission, send comment to API with username = null
createCommentMutation.mutate(
{
message,
profileId,
token,
},
{
onSuccess: () => {
setCurrentReply('');
},
},
);
} else if (status === 'authenticated') {
// If not the OP and logged in, send comment to API
createCommentMutation.mutate(
{
message,
profileId,
userId: session.user?.id,
},
{
onSuccess: () => {
setCurrentReply('');
},
},
);
} else {
// If not the OP and not logged in, direct users to log in
signIn();
}
}
function handleCopyEditLink() {
// TODO: Add notification
navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${profileId}?token=${token}`,
);
} }
function handleComment() { function handleCopyPublicLink() {
return 'profileId'; // To integrate with API navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${profileId}`,
);
} }
if (isLoading) { if (isLoading) {
return ( return (
<div className="col-span-10 pt-4"> <div className="col-span-10 pt-4">
@ -33,7 +105,7 @@ export default function ProfileComments({
); );
} }
return ( return (
<div className="m-4"> <div className="m-4 h-full">
<div className="flex-end flex justify-end space-x-4"> <div className="flex-end flex justify-end space-x-4">
{isEditable && ( {isEditable && (
<Button <Button
@ -61,57 +133,41 @@ export default function ProfileComments({
<h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2> <h2 className="mt-2 mb-6 text-2xl font-bold">Discussions</h2>
<div> <div>
<TextArea <TextArea
label="Comment as anonymous" label={`Comment as ${
isEditable ? profileName : session?.user?.name ?? 'anonymous'
}`}
placeholder="Type your comment here" placeholder="Type your comment here"
value={currentReply}
onChange={(value) => setCurrentReply(value)}
/> />
<div className="mt-2 flex w-full justify-end"> <div className="mt-2 flex w-full justify-end">
<div className="w-fit"> <div className="w-fit">
<Button <Button
disabled={commentsQuery.isLoading}
display="block" display="block"
isLabelHidden={false} isLabelHidden={false}
isLoading={createCommentMutation.isLoading}
label="Comment" label="Comment"
size="sm" size="sm"
variant="primary" variant="primary"
onClick={handleComment} onClick={() => handleComment(currentReply)}
/> />
</div> </div>
</div> </div>
<HorizontalDivider /> <HorizontalDivider />
</div> </div>
<ExpandableCommentCard <div className="h-full overflow-y-scroll">
comment={{ <div className="h-content mb-96 w-full">
createdAt: new Date(), {replies?.map((reply: Reply) => (
id: '1', <ExpandableCommentCard
message: 'Test comment', key={reply.id}
profileId: '123', comment={reply}
replies: [ profileId={profileId}
{ token={isEditable ? token : undefined}
createdAt: new Date(), />
id: '123', ))}
message: 'Test comment', </div>
profileId: '123', </div>
replies: undefined,
replyingToId: '12345',
userId: '12314',
username: 'nihao username',
},
{
createdAt: new Date(),
id: '12334',
message: 'Test comment',
profileId: '123',
replies: undefined,
replyingToId: '12345',
userId: '12314',
username: 'nihao username',
},
],
replyingToId: '12345',
userId: '12314',
username: 'nihao username',
}}
handleReply={handleReply}
/>
</div> </div>
); );
} }

@ -1,32 +1,103 @@
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react'; import { useState } from 'react';
import { ChatBubbleBottomCenterIcon } from '@heroicons/react/24/outline'; import { ChatBubbleBottomCenterIcon } from '@heroicons/react/24/outline';
import { Button, HorizontalDivider, TextArea } from '@tih/ui'; import { Button, HorizontalDivider, TextArea } from '@tih/ui';
import type { CommentEntity } from '~/components/offers/types';
import { timeSinceNow } from '~/utils/offers/time'; import { timeSinceNow } from '~/utils/offers/time';
import { trpc } from '../../../../utils/trpc';
import type { Reply } from '~/types/offers';
type Props = Readonly<{ type Props = Readonly<{
comment: CommentEntity; comment: Reply;
disableReply?: boolean;
handleExpanded?: () => void; handleExpanded?: () => void;
handleReply: (replayingToId: string, userId: string) => void; // HandleReply?: (message: string, replyingToId: string) => void;
isExpanded?: boolean; isExpanded?: boolean;
// IsLoading?: boolean;
profileId: string;
replyLength?: number; replyLength?: number;
token?: string;
}>; }>;
export default function CommentCard({ export default function CommentCard({
comment: { createdAt, message, replyingToId, userId, username }, comment: { createdAt, id, message, user },
disableReply,
handleExpanded, handleExpanded,
handleReply, // HandleReply,
isExpanded, isExpanded,
// IsLoading,
profileId,
token = '',
replyLength = 0, replyLength = 0,
}: Props) { }: Props) {
const { data: session, status } = useSession();
const [isReplying, setIsReplying] = useState(false); const [isReplying, setIsReplying] = useState(false);
const [currentReply, setCurrentReply] = useState<string>('');
const trpcContext = trpc.useContext();
const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
onSuccess() {
trpcContext.invalidateQueries([
'offers.comments.getComments',
{ profileId },
]);
},
});
function handleReply() {
if (token && token.length > 0) {
// If it is with edit permission, send comment to API with username = null
createCommentMutation.mutate(
{
message: currentReply,
profileId,
replyingToId: id,
token,
},
{
onSuccess: () => {
setCurrentReply('');
setIsReplying(false);
if (!isExpanded) {
handleExpanded?.();
}
},
},
);
} else if (status === 'authenticated') {
// If not the OP and logged in, send comment to API
createCommentMutation.mutate(
{
message: currentReply,
profileId,
replyingToId: id,
userId: session.user?.id,
},
{
onSuccess: () => {
setCurrentReply('');
setIsReplying(false);
if (!isExpanded) {
handleExpanded?.();
}
},
},
);
} else {
// If not the OP and not logged in, direct users to log in
signIn();
}
}
return ( return (
<> <>
<div className="flex pl-2"> <div className="flex pl-2">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div className="flex flex-row font-bold">{username}</div> <div className="flex flex-row font-bold">
{user?.name ?? 'unknown user'}
</div>
<div className="mt-2 mb-2 flex flex-row ">{message}</div> <div className="mt-2 mb-2 flex flex-row ">{message}</div>
<div className="flex flex-row items-center justify-start space-x-4 "> <div className="flex flex-row items-center justify-start space-x-4 ">
<div className="flex flex-col text-sm font-light text-gray-400">{`${timeSinceNow( <div className="flex flex-col text-sm font-light text-gray-400">{`${timeSinceNow(
@ -39,34 +110,39 @@ export default function CommentCard({
{isExpanded ? `Hide replies` : `View replies (${replyLength})`} {isExpanded ? `Hide replies` : `View replies (${replyLength})`}
</div> </div>
)} )}
<div className="flex flex-col"> {!disableReply && (
<Button <div className="flex flex-col">
icon={ChatBubbleBottomCenterIcon} <Button
isLabelHidden={true} icon={ChatBubbleBottomCenterIcon}
label="Reply" isLabelHidden={true}
size="sm" label="Reply"
variant="tertiary" size="sm"
onClick={() => setIsReplying(!isReplying)} variant="tertiary"
/> onClick={() => setIsReplying(!isReplying)}
</div> />
</div>
)}
</div> </div>
{isReplying && ( {!disableReply && isReplying && (
<div className="mt-2"> <div className="mt-2 mr-2">
<TextArea <TextArea
isLabelHidden={true} isLabelHidden={true}
label="Comment" label="Comment"
placeholder="Type your comment here" placeholder="Type your comment here"
resize="none" resize="none"
value={currentReply}
onChange={(value) => setCurrentReply(value)}
/> />
<div className="mt-2 flex w-full justify-end"> <div className="mt-2 flex w-full justify-end">
<div className="w-fit"> <div className="w-fit">
<Button <Button
display="block" display="block"
isLabelHidden={false} isLabelHidden={false}
isLoading={createCommentMutation.isLoading}
label="Reply" label="Reply"
size="sm" size="sm"
variant="primary" variant="primary"
onClick={() => handleReply(replyingToId, userId)} onClick={handleReply}
/> />
</div> </div>
</div> </div>

@ -1,23 +1,36 @@
import { useState } from 'react'; import { useState } from 'react';
import CommentCard from '~/components/offers/profile/comments/CommentCard'; import CommentCard from '~/components/offers/profile/comments/CommentCard';
import type { CommentEntity } from '~/components/offers/types';
import type { Reply } from '~/types/offers';
type Props = Readonly<{ type Props = Readonly<{
comment: CommentEntity; comment: Reply;
handleReply: (replayingToId: string, userId: string) => void; // HandleReply?: (message: string, replyingToId: string) => void;
// isLoading?: boolean;
profileId: string;
token?: string;
}>; }>;
export default function ExpandableCommentCard({ comment, handleReply }: Props) { export default function ExpandableCommentCard({
comment,
profileId,
token = '',
}: // HandleReply,
// isLoading,
Props) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
return ( return (
<div> <div>
<CommentCard <CommentCard
comment={comment} comment={comment}
handleExpanded={() => setIsExpanded(!isExpanded)} handleExpanded={() => setIsExpanded(!isExpanded)}
handleReply={handleReply} // HandleReply={handleReply}
isExpanded={isExpanded} isExpanded={isExpanded}
profileId={profileId}
replyLength={comment.replies?.length ?? 0} replyLength={comment.replies?.length ?? 0}
// IsLoading={isLoading}
token={token}
/> />
{comment.replies && ( {comment.replies && (
<div className="pl-8"> <div className="pl-8">
@ -26,7 +39,8 @@ export default function ExpandableCommentCard({ comment, handleReply }: Props) {
<CommentCard <CommentCard
key={reply.id} key={reply.id}
comment={reply} comment={reply}
handleReply={handleReply} disableReply={true}
profileId={profileId} // IsLoading={isLoading}
/> />
))} ))}
</div> </div>

@ -5,10 +5,3 @@ export enum YOE_CATEGORY {
MID = 2, MID = 2,
SENIOR = 3, SENIOR = 3,
} }
export type PaginationType = {
currentPage: number;
numOfItems: number;
numOfPages: number;
totalItems: number;
};

@ -10,6 +10,9 @@ import type { BackgroundCard, OfferEntity } from '~/components/offers/types';
import { convertCurrencyToString } from '~/utils/offers/currency'; import { convertCurrencyToString } from '~/utils/offers/currency';
import { formatDate } from '~/utils/offers/time'; import { formatDate } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
import type { Profile, ProfileOffer } from '~/types/offers';
export default function OfferProfile() { export default function OfferProfile() {
const ErrorPage = ( const ErrorPage = (
<Error statusCode={404} title="Requested profile does not exist." /> <Error statusCode={404} title="Requested profile does not exist." />
@ -29,7 +32,7 @@ export default function OfferProfile() {
], ],
{ {
enabled: typeof offerProfileId === 'string', enabled: typeof offerProfileId === 'string',
onSuccess: (data) => { onSuccess: (data: Profile) => {
if (!data) { if (!data) {
router.push('/offers'); router.push('/offers');
} }
@ -42,7 +45,7 @@ export default function OfferProfile() {
if (data?.offers) { if (data?.offers) {
const filteredOffers: Array<OfferEntity> = data const filteredOffers: Array<OfferEntity> = data
? data?.offers.map((res) => { ? data?.offers.map((res: ProfileOffer) => {
if (res.offersFullTime) { if (res.offersFullTime) {
const filteredOffer: OfferEntity = { const filteredOffer: OfferEntity = {
base: convertCurrencyToString( base: convertCurrencyToString(
@ -153,19 +156,6 @@ export default function OfferProfile() {
} }
} }
function handleCopyEditLink() {
// TODO: Add notification
navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${offerProfileId}?token=${token}`,
);
}
function handleCopyPublicLink() {
navigator.clipboard.writeText(
`${window.location.origin}/offers/profile/${offerProfileId}`,
);
}
return ( return (
<> <>
{getProfileQuery.isError && ErrorPage} {getProfileQuery.isError && ErrorPage}
@ -191,11 +181,12 @@ export default function OfferProfile() {
</div> </div>
<div className="h-full w-1/3 bg-white"> <div className="h-full w-1/3 bg-white">
<ProfileComments <ProfileComments
handleCopyEditLink={handleCopyEditLink}
handleCopyPublicLink={handleCopyPublicLink}
isDisabled={deleteMutation.isLoading} isDisabled={deleteMutation.isLoading}
isEditable={isEditable} isEditable={isEditable}
isLoading={getProfileQuery.isLoading} isLoading={getProfileQuery.isLoading}
profileId={offerProfileId as string}
profileName={background?.profileName}
token={token as string}
/> />
</div> </div>
</div> </div>

Loading…
Cancel
Save