adding replies toggle, filtering out responses that don't have replies
This commit is contained in:
parent
c115b5cca7
commit
8dd5b477d9
23
web/components/comments/comments.tsx
Normal file
23
web/components/comments/comments.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
|
||||||
|
export function ReplyToggle(props: {
|
||||||
|
seeReplies: boolean
|
||||||
|
numComments: number
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
const { seeReplies, numComments, onClick } = props
|
||||||
|
return (
|
||||||
|
<button className="text-left text-sm text-indigo-600" onClick={onClick}>
|
||||||
|
<Row className="items-center gap-1">
|
||||||
|
<div>
|
||||||
|
{numComments} {numComments === 1 ? 'Reply' : 'Replies'}
|
||||||
|
</div>
|
||||||
|
<TriangleDownFillIcon
|
||||||
|
className={clsx('h-2 w-2', seeReplies ? 'rotate-180' : '')}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import {
|
||||||
usePersistentState,
|
usePersistentState,
|
||||||
} from 'web/hooks/use-persistent-state'
|
} from 'web/hooks/use-persistent-state'
|
||||||
import { safeLocalStorage } from 'web/lib/util/local'
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
|
|
||||||
export function ContractTabs(props: {
|
export function ContractTabs(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -123,24 +124,28 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
||||||
const topLevelComments = commentsByParent['_'] ?? []
|
const topLevelComments = commentsByParent['_'] ?? []
|
||||||
|
|
||||||
const sortRow = comments.length > 0 && (
|
const sortRow = comments.length > 0 && (
|
||||||
<Row className="mb-4 items-center">
|
<Row className="mb-4 items-center justify-end gap-4">
|
||||||
<Button
|
|
||||||
size={'xs'}
|
|
||||||
color={'gray-white'}
|
|
||||||
onClick={() => setSort(sort === 'Newest' ? 'Best' : 'Newest')}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
text={
|
|
||||||
sort === 'Best'
|
|
||||||
? 'Highest tips + bounties first. Your new comments briefly appear to you first.'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Sort by: {sort}
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<BountiedContractSmallBadge contract={contract} showAmount />
|
<BountiedContractSmallBadge contract={contract} showAmount />
|
||||||
|
<Row className="items-center gap-1">
|
||||||
|
<div className="text-greyscale-4 text-sm">Sort by:</div>
|
||||||
|
<button
|
||||||
|
className="text-greyscale-6 w-20 text-sm"
|
||||||
|
onClick={() => setSort(sort === 'Newest' ? 'Best' : 'Newest')}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
text={
|
||||||
|
sort === 'Best'
|
||||||
|
? 'Highest tips + bounties first. Your new comments briefly appear to you first.'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Row className="items-center gap-1">
|
||||||
|
{sort}
|
||||||
|
<TriangleDownFillIcon className=" h-2 w-2" />
|
||||||
|
</Row>
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,24 +164,32 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{sortRow}
|
<Col className="flex w-full">
|
||||||
{sortedAnswers.map((answer) => (
|
<div className="mb-4 w-full border-gray-200" />
|
||||||
<div key={answer.id} className="relative pb-4">
|
{sortedAnswers.map((answer) => {
|
||||||
<span
|
const answerComments =
|
||||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
commentsByOutcome[answer.number.toString()] ?? []
|
||||||
aria-hidden="true"
|
if (answerComments.length > 0) {
|
||||||
/>
|
return (
|
||||||
<FeedAnswerCommentGroup
|
<div key={answer.id} className="relative pb-4">
|
||||||
contract={contract}
|
<span
|
||||||
answer={answer}
|
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||||
answerComments={commentsByOutcome[answer.number.toString()] ?? []}
|
aria-hidden="true"
|
||||||
tips={tips}
|
/>
|
||||||
/>
|
<FeedAnswerCommentGroup
|
||||||
</div>
|
contract={contract}
|
||||||
))}
|
answer={answer}
|
||||||
<Col className="mt-8 flex w-full">
|
answerComments={
|
||||||
<div className="text-md mt-8 mb-2 text-left">General Comments</div>
|
commentsByOutcome[answer.number.toString()] ?? []
|
||||||
<div className="mb-4 w-full border-b border-gray-200" />
|
}
|
||||||
|
tips={tips}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
})}
|
||||||
{sortRow}
|
{sortRow}
|
||||||
<ContractCommentInput className="mb-5" contract={contract} />
|
<ContractCommentInput className="mb-5" contract={contract} />
|
||||||
{generalTopLevelComments.map((comment) => (
|
{generalTopLevelComments.map((comment) => (
|
||||||
|
|
|
@ -16,6 +16,8 @@ import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-ti
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
|
import { ReplyToggle } from '../comments/comments'
|
||||||
|
|
||||||
export function FeedAnswerCommentGroup(props: {
|
export function FeedAnswerCommentGroup(props: {
|
||||||
contract: FreeResponseContract
|
contract: FreeResponseContract
|
||||||
|
@ -26,6 +28,8 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const { answer, contract, answerComments, tips } = props
|
const { answer, contract, answerComments, tips } = props
|
||||||
const { username, avatarUrl, name, text } = answer
|
const { username, avatarUrl, name, text } = answer
|
||||||
|
|
||||||
|
const [seeReplies, setSeeReplies] = useState(false)
|
||||||
|
|
||||||
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
const [replyTo, setReplyTo] = useState<ReplyTo>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const answerElementId = `answer-${answer.id}`
|
const answerElementId = `answer-${answer.id}`
|
||||||
|
@ -37,7 +41,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
answerRef.current.scrollIntoView(true)
|
answerRef.current.scrollIntoView(true)
|
||||||
}
|
}
|
||||||
}, [highlighted])
|
}, [highlighted])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="relative flex-1 items-stretch gap-3">
|
<Col className="relative flex-1 items-stretch gap-3">
|
||||||
<Row
|
<Row
|
||||||
|
@ -49,7 +52,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
id={answerElementId}
|
id={answerElementId}
|
||||||
>
|
>
|
||||||
<Avatar username={username} avatarUrl={avatarUrl} />
|
<Avatar username={username} avatarUrl={avatarUrl} />
|
||||||
|
|
||||||
<Col className="min-w-0 flex-1 lg:gap-1">
|
<Col className="min-w-0 flex-1 lg:gap-1">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<UserLink username={username} name={name} /> answered
|
<UserLink username={username} name={name} /> answered
|
||||||
|
@ -60,12 +62,11 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
elementId={answerElementId}
|
elementId={answerElementId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Row className="align-items justify-between gap-2 sm:flex-row">
|
||||||
<Col className="align-items justify-between gap-2 sm:flex-row">
|
|
||||||
<span className="whitespace-pre-line text-lg">
|
<span className="whitespace-pre-line text-lg">
|
||||||
<Linkify text={text} />
|
<Linkify text={text} />
|
||||||
</span>
|
</span>
|
||||||
<div className="sm:hidden">
|
<div>
|
||||||
<button
|
<button
|
||||||
className="text-xs font-bold text-gray-500 hover:underline"
|
className="text-xs font-bold text-gray-500 hover:underline"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -75,33 +76,30 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Row>
|
||||||
<div className="justify-initial hidden sm:block">
|
<ReplyToggle
|
||||||
<button
|
seeReplies={seeReplies}
|
||||||
className="text-xs font-bold text-gray-500 hover:underline"
|
numComments={answerComments.length}
|
||||||
onClick={() =>
|
onClick={() => setSeeReplies(!seeReplies)}
|
||||||
setReplyTo({ id: answer.id, username: answer.username })
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
Reply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Col className="gap-3 pl-1">
|
{seeReplies && (
|
||||||
{answerComments.map((comment) => (
|
<Col className="gap-3 pl-1">
|
||||||
<FeedComment
|
{answerComments.map((comment) => (
|
||||||
key={comment.id}
|
<FeedComment
|
||||||
indent={true}
|
key={comment.id}
|
||||||
contract={contract}
|
indent={true}
|
||||||
comment={comment}
|
contract={contract}
|
||||||
tips={tips[comment.id] ?? {}}
|
comment={comment}
|
||||||
onReplyClick={() =>
|
tips={tips[comment.id] ?? {}}
|
||||||
setReplyTo({ id: comment.id, username: comment.userUsername })
|
onReplyClick={() =>
|
||||||
}
|
setReplyTo({ id: comment.id, username: comment.userUsername })
|
||||||
/>
|
}
|
||||||
))}
|
/>
|
||||||
</Col>
|
))}
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
{replyTo && (
|
{replyTo && (
|
||||||
<div className="relative ml-7">
|
<div className="relative ml-7">
|
||||||
<span
|
<span
|
||||||
|
|
Loading…
Reference in New Issue
Block a user