mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-04-08 05:18:19 +08:00
[questions][feat] tweak cards UI (#546)
* [questions][feat] tweak card UI * [questions][feat] tweak comment and answer pages
This commit is contained in:
@@ -106,7 +106,7 @@ export default function AddToListDropdown({
|
||||
|
||||
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
|
||||
<button
|
||||
className="focus:ring-primary-500 inline-flex w-full justify-center 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
|
||||
className="focus:ring-primary-500 inline-flex w-full items-center justify-center rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
|
||||
type="button"
|
||||
onClick={handleMenuButtonClick}>
|
||||
{children}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import { Badge } from '@tih/ui';
|
||||
|
||||
import 'react-popper-tooltip/dist/styles.css';
|
||||
|
||||
type BadgeProps = ComponentProps<typeof Badge>;
|
||||
|
||||
export type QuestionAggregateBadgeProps = Omit<BadgeProps, 'label'> & {
|
||||
export type QuestionAggregateBadgeProps = Readonly<{
|
||||
icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
|
||||
statistics: Record<string, number>;
|
||||
};
|
||||
}>;
|
||||
|
||||
export default function QuestionAggregateBadge({
|
||||
icon: Icon,
|
||||
statistics,
|
||||
...badgeProps
|
||||
}: QuestionAggregateBadgeProps) {
|
||||
const { getTooltipProps, setTooltipRef, setTriggerRef, visible } =
|
||||
usePopperTooltip({
|
||||
@@ -56,8 +53,16 @@ export default function QuestionAggregateBadge({
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={setTriggerRef} className="rounded-full" type="button">
|
||||
<Badge label={label} {...badgeProps} />
|
||||
<button
|
||||
ref={setTriggerRef}
|
||||
className="-my-1 flex items-center rounded-md px-2
|
||||
py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button">
|
||||
<Icon
|
||||
aria-hidden={true}
|
||||
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
|
||||
/>
|
||||
{label}
|
||||
</button>
|
||||
{visible && (
|
||||
<div ref={setTooltipRef} {...getTooltipProps()} className="z-10">
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function SortOptionsSelect({
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
display="inline"
|
||||
label="Sort by"
|
||||
@@ -52,7 +52,7 @@ export default function SortOptionsSelect({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
display="inline"
|
||||
label="Order by"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
|
||||
import type { Vote } from '@prisma/client';
|
||||
import type { ButtonSize } from '@tih/ui';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
|
||||
|
||||
@@ -18,7 +17,7 @@ export type VotingButtonsCallbackProps = {
|
||||
};
|
||||
|
||||
export type VotingButtonsProps = VotingButtonsCallbackProps & {
|
||||
size?: ButtonSize;
|
||||
size?: 'md' | 'sm';
|
||||
upvoteCount: number;
|
||||
};
|
||||
|
||||
@@ -29,11 +28,6 @@ export default function VotingButtons({
|
||||
upvoteCount,
|
||||
size = 'md',
|
||||
}: VotingButtonsProps) {
|
||||
const upvoteButtonVariant =
|
||||
vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary';
|
||||
const downvoteButtonVariant =
|
||||
vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary';
|
||||
|
||||
const handleUpvoteClick = useProtectedCallback(() => {
|
||||
onUpvote();
|
||||
});
|
||||
@@ -44,31 +38,45 @@ export default function VotingButtons({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Button
|
||||
icon={ChevronUpIcon}
|
||||
isLabelHidden={true}
|
||||
label="Upvote"
|
||||
size={size}
|
||||
variant={upvoteButtonVariant}
|
||||
<button
|
||||
aria-label="Upvote"
|
||||
className="rounded-full p-1 hover:bg-slate-100"
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUpvoteClick();
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<ChevronUpIcon
|
||||
className={clsx(
|
||||
size === 'sm' && 'h-5 w-5',
|
||||
size === 'md' && 'h-8 w-8',
|
||||
vote?.vote === 'UPVOTE'
|
||||
? 'text-primary-500'
|
||||
: 'hover:text-primary-500 text-slate-400',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<p>{upvoteCount}</p>
|
||||
<Button
|
||||
icon={ChevronDownIcon}
|
||||
isLabelHidden={true}
|
||||
label="Downvote"
|
||||
size={size}
|
||||
variant={downvoteButtonVariant}
|
||||
<button
|
||||
aria-label="Downvote"
|
||||
className="rounded-full p-1 hover:bg-slate-100"
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDownvoteClick();
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<ChevronDownIcon
|
||||
className={clsx(
|
||||
size === 'sm' && 'h-5 w-5',
|
||||
size === 'md' && 'h-8 w-8',
|
||||
vote?.vote === 'DOWNVOTE'
|
||||
? 'text-danger-500'
|
||||
: 'hover:text-danger-500 text-slate-400',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { format } from 'date-fns';
|
||||
import clsx from 'clsx';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { ChatBubbleLeftRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
import useAnswerVote from '~/utils/questions/vote/useAnswerVote';
|
||||
@@ -30,11 +31,13 @@ export default function AnswerCard({
|
||||
showHover,
|
||||
}: AnswerCardProps) {
|
||||
const { handleUpvote, handleDownvote, vote } = useAnswerVote(answerId);
|
||||
const hoverClass = showHover ? 'hover:bg-slate-50' : '';
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`flex gap-4 rounded-md border bg-white p-2 ${hoverClass}`}>
|
||||
className={clsx(
|
||||
'flex gap-4 rounded-md border border-slate-200 bg-white p-4 sm:rounded-lg',
|
||||
showHover && 'hover:bg-slate-50',
|
||||
)}>
|
||||
<VotingButtons
|
||||
size={votingButtonsSize}
|
||||
upvoteCount={upvoteCount}
|
||||
@@ -42,26 +45,37 @@ export default function AnswerCard({
|
||||
onDownvote={handleDownvote}
|
||||
onUpvote={handleUpvote}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${authorName} profile picture`}
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={authorImageUrl}></img>
|
||||
<h1 className="font-bold">{authorName}</h1>
|
||||
<p className="text-xs font-extralight">
|
||||
Posted on: {format(createdAt, 'h:mm a, MMMM dd, yyyy')}
|
||||
alt={authorName}
|
||||
className="h-7 w-7 rounded-full"
|
||||
src={authorImageUrl}
|
||||
/>
|
||||
<p className="text-sm font-medium text-slate-900">{authorName}</p>
|
||||
<span className="font-medium text-slate-500">·</span>
|
||||
<p className="text-xs text-slate-500">
|
||||
{formatDistanceToNow(createdAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p className="whitespace-pre-line">{content}</p>
|
||||
{commentCount !== undefined && (
|
||||
<div className="flex items-center gap-2 text-slate-500">
|
||||
<ChatBubbleLeftRightIcon className="h-6 w-6" />
|
||||
<p className="text-sm font-medium">
|
||||
<p className="whitespace-pre-wrap text-xs sm:text-sm">{content}</p>
|
||||
<div className="-ml-2">
|
||||
{commentCount !== undefined && (
|
||||
<button
|
||||
className="-my-1 flex items-center rounded-md px-2
|
||||
py-1 text-xs font-medium
|
||||
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button">
|
||||
<ChatBubbleLeftRightIcon
|
||||
aria-hidden={true}
|
||||
className="mr-2 h-5 w-5"
|
||||
/>
|
||||
{commentCount} {commentCount === 1 ? 'comment' : 'comments'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
BuildingOfficeIcon,
|
||||
MapPinIcon,
|
||||
UserCircleIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import {
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
CheckIcon,
|
||||
@@ -198,21 +203,24 @@ export default function BaseQuestionCard({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<div className="flex items-baseline justify-between self-stretch">
|
||||
<div className="flex flex-wrap items-center gap-2 text-slate-500">
|
||||
<div className="flex flex-1 flex-col items-start gap-4">
|
||||
<div className="flex items-center justify-between self-stretch">
|
||||
<div className="flex flex-wrap items-center gap-3 text-slate-500">
|
||||
{showAggregateStatistics && (
|
||||
<>
|
||||
<QuestionTypeBadge type={type} />
|
||||
<QuestionAggregateBadge
|
||||
icon={BuildingOfficeIcon}
|
||||
statistics={companies}
|
||||
variant="primary"
|
||||
/>
|
||||
<QuestionAggregateBadge
|
||||
icon={MapPinIcon}
|
||||
statistics={locations!}
|
||||
variant="success"
|
||||
/>
|
||||
<QuestionAggregateBadge statistics={roles} variant="danger" />
|
||||
<QuestionAggregateBadge
|
||||
icon={UserCircleIcon}
|
||||
statistics={roles}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{timestamp !== null && (
|
||||
@@ -240,59 +248,43 @@ export default function BaseQuestionCard({
|
||||
</div>
|
||||
<p
|
||||
className={clsx(
|
||||
'whitespace-pre-line font-semibold',
|
||||
'whitespace-pre-line text-base font-medium leading-6 text-slate-900 md:text-lg',
|
||||
truncateContent && 'line-clamp-2 text-ellipsis',
|
||||
)}>
|
||||
{content}
|
||||
</p>
|
||||
|
||||
{!showReceivedForm &&
|
||||
(showAnswerStatistics ||
|
||||
showReceivedStatistics ||
|
||||
showCreateEncounterButton) && (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex w-full gap-4">
|
||||
{showAnswerStatistics && (
|
||||
<>
|
||||
<div className="sm:hidden">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={ChatBubbleBottomCenterTextIcon}
|
||||
label={`${answerCount}`}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
<div>
|
||||
<button
|
||||
className="-my-1 flex items-center rounded-md px-2
|
||||
py-1 text-xs font-medium
|
||||
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button">
|
||||
<ChatBubbleBottomCenterTextIcon
|
||||
aria-hidden={true}
|
||||
className="mr-2 h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={ChatBubbleBottomCenterTextIcon}
|
||||
label={`${answerCount} answers`}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{answerCount} {answerCount === 1 ? 'answer' : 'answers'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showReceivedStatistics && (
|
||||
<>
|
||||
<div className="sm:hidden">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={EyeIcon}
|
||||
label={`${receivedCount}`}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
icon={EyeIcon}
|
||||
label={`${receivedCount} received this`}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<button
|
||||
className="-my-1 flex items-center rounded-md px-2
|
||||
py-1 text-xs font-medium
|
||||
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
|
||||
type="button">
|
||||
<EyeIcon aria-hidden={true} className="mr-2 h-5 w-5" />
|
||||
{receivedCount} received this
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showCreateEncounterButton && (
|
||||
<Button
|
||||
@@ -323,9 +315,9 @@ export default function BaseQuestionCard({
|
||||
return (
|
||||
<article
|
||||
className={clsx(
|
||||
'group flex gap-4 border-slate-300',
|
||||
showHover && 'transition hover:bg-slate-50',
|
||||
!hideCard && 'rounded-md border bg-white p-4',
|
||||
'group flex gap-4 border-slate-200',
|
||||
showHover && 'hover:border-primary-500 transition',
|
||||
!hideCard && 'rounded-md border bg-white p-4 sm:rounded-lg sm:p-6',
|
||||
)}>
|
||||
{cardContent}
|
||||
{showDeleteButton && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
import type { BackendVote } from '../VotingButtons';
|
||||
import VotingButtons from '../VotingButtons';
|
||||
@@ -25,7 +25,7 @@ export default function CommentListItem({
|
||||
onUpvote,
|
||||
}: CommentListItemProps) {
|
||||
return (
|
||||
<div className="flex gap-4 rounded-md border bg-white p-2">
|
||||
<div className="flex gap-4 rounded-md border border-slate-200 bg-white p-2">
|
||||
<VotingButtons
|
||||
size="sm"
|
||||
upvoteCount={upvoteCount}
|
||||
@@ -36,15 +36,19 @@ export default function CommentListItem({
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={`${authorName} profile picture`}
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={authorImageUrl}></img>
|
||||
<h1 className="font-bold">{authorName}</h1>
|
||||
<p className="pt-1 text-xs font-extralight">
|
||||
Posted on: {format(createdAt, 'h:mm a, MMMM dd, yyyy')}
|
||||
alt={authorName}
|
||||
className="h-7 w-7 rounded-full"
|
||||
src={authorImageUrl}
|
||||
/>
|
||||
<p className="text-sm font-medium text-slate-900">{authorName}</p>
|
||||
<span className="font-medium text-slate-500">·</span>
|
||||
<p className="text-xs text-slate-500">
|
||||
{formatDistanceToNow(createdAt, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<p className="whitespace-pre-line">{content}</p>
|
||||
<p className="whitespace-pre-wrap text-xs sm:text-sm">{content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -142,6 +142,7 @@ export default function CreateQuestionEncounterForm({
|
||||
(step === 2 && selectedRole === null)
|
||||
}
|
||||
label="Next"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setStep(step + 1);
|
||||
@@ -152,6 +153,7 @@ export default function CreateQuestionEncounterForm({
|
||||
<Button
|
||||
isLoading={loading}
|
||||
label="Submit"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={async () => {
|
||||
if (
|
||||
@@ -181,6 +183,7 @@ export default function CreateQuestionEncounterForm({
|
||||
)}
|
||||
<Button
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { PropsWithChildren } from 'react';
|
||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
import Container from '~/components/shared/Container';
|
||||
|
||||
export type BackButtonLayoutProps = PropsWithChildren<{
|
||||
href: string;
|
||||
}>;
|
||||
@@ -11,7 +13,7 @@ export default function BackButtonLayout({
|
||||
children,
|
||||
}: BackButtonLayoutProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-1 flex-col items-stretch gap-4 p-4 lg:flex-row">
|
||||
<Container className="flex flex-col gap-4 pt-4 pb-12" variant="sm">
|
||||
<div>
|
||||
<Button
|
||||
addonPosition="start"
|
||||
@@ -19,12 +21,13 @@ export default function BackButtonLayout({
|
||||
href={href}
|
||||
icon={ArrowSmallLeftIcon}
|
||||
label="Back"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +44,12 @@ export default function ExpandedTypeahead({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{suggestions.map((suggestion) => (
|
||||
<div key={suggestion.id} className="hidden lg:block">
|
||||
<Button
|
||||
label={suggestion.label}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
onSuggestionClick?.(suggestion);
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function QuestionPage() {
|
||||
</Head>
|
||||
<BackButtonLayout
|
||||
href={`/questions/${router.query.questionId}/${router.query.questionSlug}`}>
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-4">
|
||||
<FullAnswerCard
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
@@ -122,7 +122,7 @@ export default function QuestionPage() {
|
||||
createdAt={answer.createdAt}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
<div className="mx-2">
|
||||
<div>
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
@@ -136,7 +136,7 @@ export default function QuestionPage() {
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<div className="my-3 flex justify-end">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
@@ -145,7 +145,7 @@ export default function QuestionPage() {
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<div className="flex items-end gap-2">
|
||||
|
||||
@@ -200,8 +200,8 @@ export default function QuestionPage() {
|
||||
</title>
|
||||
</Head>
|
||||
<BackButtonLayout href="/questions/browse">
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 rounded-md border bg-white p-4">
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 rounded-md border bg-white p-4">
|
||||
<FullQuestionCard
|
||||
{...question}
|
||||
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
||||
@@ -224,24 +224,24 @@ export default function QuestionPage() {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="ml-16 mr-2">
|
||||
<div className="sm:ml-13 ml-11 mr-2 md:ml-14">
|
||||
<Collapsible
|
||||
defaultOpen={true}
|
||||
label={
|
||||
<div className="text-primary-700">{`${question.numComments} comment(s)`}</div>
|
||||
}>
|
||||
<div className="">
|
||||
<div className="flex flex-col gap-2 text-black">
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
sortTypeValue={commentSortType}
|
||||
onSortOrderChange={setCommentSortOrder}
|
||||
onSortTypeChange={setCommentSortType}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-black">
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
sortTypeValue={commentSortType}
|
||||
onSortOrderChange={setCommentSortOrder}
|
||||
onSortTypeChange={setCommentSortType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{(commentData?.pages ?? []).flatMap(({ data: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<QuestionCommentListItem
|
||||
@@ -255,30 +255,30 @@ export default function QuestionPage() {
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={commentInfiniteQuery} />
|
||||
<form
|
||||
className="mt-4"
|
||||
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<PaginationLoadMoreButton query={commentInfiniteQuery} />
|
||||
<form
|
||||
className="mt-4"
|
||||
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-end">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
@@ -299,7 +299,7 @@ export default function QuestionPage() {
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 mb-1 flex justify-between">
|
||||
<div className="mt-3 mb-1 flex justify-end">
|
||||
<Button
|
||||
disabled={!isDirty || !isValid}
|
||||
label="Contribute"
|
||||
@@ -308,11 +308,11 @@ export default function QuestionPage() {
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="text-xl font-semibold">
|
||||
{question.numAnswers} answers
|
||||
</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex items-end gap-4">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={answerSortOrder}
|
||||
sortTypeValue={answerSortType}
|
||||
@@ -321,25 +321,27 @@ export default function QuestionPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Add button to load more */}
|
||||
{(answerData?.pages ?? []).flatMap(({ data: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* TODO: Add button to load more */}
|
||||
{(answerData?.pages ?? []).flatMap(({ data: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||
</div>
|
||||
</div>
|
||||
</BackButtonLayout>
|
||||
</>
|
||||
|
||||
@@ -541,7 +541,7 @@ export default function QuestionsBrowsePage() {
|
||||
onSortTypeChange={setSortType}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 pb-4">
|
||||
<div className="flex flex-col gap-4 pb-4">
|
||||
{(questionsQueryData?.pages ?? []).flatMap(
|
||||
({ data: questions }) =>
|
||||
questions.map((question) => {
|
||||
|
||||
Reference in New Issue
Block a user