mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
feat: Add comment threads to challenges (#2026)
* feat: Add comment threads to challenges Wire up the full comment/discussion system for challenges: - Add Thread <-> Challenge relation in Prisma schema with migration - Create ChallengeDiscussion component with title, sort filter, hidden comments support - Add comment count to challenge list items, detail page, and active events - Support challenge entity type in comment connector, hidden toggle, and hidden comments modal - Add challenge comment notifications (new-challenge-comment) with proper thread URL mapping - Show comment count badge on ChallengeCard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Smol fixes * Applies code review feedback * fix: Use correct owner field when hiding challenge comments Challenge uses `createdById` not `userId` for its owner field. The dynamic Prisma select in toggleHideCommentHandler was always selecting `userId`, causing a validation error for challenge threads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
9bddb5d25e
commit
f251a5b378
@@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Thread" ADD COLUMN "challengeId" INTEGER;
|
||||
|
||||
-- CreateIndex (unique constraint)
|
||||
CREATE UNIQUE INDEX "Thread_challengeId_key" ON "Thread"("challengeId");
|
||||
|
||||
-- CreateIndex (hash index for lookups)
|
||||
CREATE INDEX "Thread_challengeId_idx" ON "Thread" USING HASH ("challengeId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Thread" ADD CONSTRAINT "Thread_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -2112,6 +2112,8 @@ model Thread {
|
||||
bountyEntry BountyEntry? @relation(fields: [bountyEntryId], references: [id], onDelete: SetNull)
|
||||
clubPostId Int? @unique
|
||||
clubPost ClubPost? @relation(fields: [clubPostId], references: [id], onDelete: SetNull)
|
||||
challengeId Int? @unique
|
||||
challenge Challenge? @relation(fields: [challengeId], references: [id], onDelete: SetNull)
|
||||
|
||||
metadata Json @default("{}") // unused
|
||||
commentCount Int @default(0)
|
||||
@@ -2126,6 +2128,7 @@ model Thread {
|
||||
@@index([imageId], type: Hash)
|
||||
@@index([articleId], type: Hash)
|
||||
@@index([rootThreadId], type: Hash)
|
||||
@@index([challengeId], type: Hash)
|
||||
}
|
||||
|
||||
model QuestionReaction {
|
||||
@@ -3791,6 +3794,7 @@ model Challenge {
|
||||
|
||||
// Relations
|
||||
winners ChallengeWinner[]
|
||||
threads Thread[]
|
||||
|
||||
// Event grouping
|
||||
eventId Int?
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { BadgeProps } from '@mantine/core';
|
||||
import { Badge, Group, Text } from '@mantine/core';
|
||||
import { IconClockHour4, IconPhoto, IconTrophy, IconSparkles } from '@tabler/icons-react';
|
||||
import {
|
||||
IconClockHour4,
|
||||
IconMessageCircle2,
|
||||
IconPhoto,
|
||||
IconTrophy,
|
||||
IconSparkles,
|
||||
} from '@tabler/icons-react';
|
||||
import cardClasses from '~/components/Cards/Cards.module.css';
|
||||
import { CurrencyBadge } from '~/components/Currency/CurrencyBadge';
|
||||
import { IconBadge } from '~/components/IconBadge/IconBadge';
|
||||
@@ -82,6 +88,7 @@ export function ChallengeCard({ data }: Props) {
|
||||
source,
|
||||
prizePool,
|
||||
entryCount,
|
||||
commentCount,
|
||||
createdBy,
|
||||
} = data;
|
||||
|
||||
@@ -166,21 +173,40 @@ export function ChallengeCard({ data }: Props) {
|
||||
className={cardClasses.chip}
|
||||
style={darkBgStyle}
|
||||
/>
|
||||
<IconBadge
|
||||
icon={<IconPhoto size={14} />}
|
||||
color="gray.0"
|
||||
p={0}
|
||||
px={8}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
className={cardClasses.chip}
|
||||
style={darkBgStyle}
|
||||
radius="xl"
|
||||
>
|
||||
<Text fw="bold" size="xs">
|
||||
{abbreviateNumber(entryCount)}
|
||||
</Text>
|
||||
</IconBadge>
|
||||
<Group gap={4}>
|
||||
{commentCount > 0 && (
|
||||
<IconBadge
|
||||
icon={<IconMessageCircle2 size={14} />}
|
||||
color="gray.0"
|
||||
p={0}
|
||||
px={8}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
className={cardClasses.chip}
|
||||
style={darkBgStyle}
|
||||
radius="xl"
|
||||
>
|
||||
<Text fw="bold" size="xs">
|
||||
{abbreviateNumber(commentCount)}
|
||||
</Text>
|
||||
</IconBadge>
|
||||
)}
|
||||
<IconBadge
|
||||
icon={<IconPhoto size={14} />}
|
||||
color="gray.0"
|
||||
p={0}
|
||||
px={8}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
className={cardClasses.chip}
|
||||
style={darkBgStyle}
|
||||
radius="xl"
|
||||
>
|
||||
<Text fw="bold" size="xs">
|
||||
{abbreviateNumber(entryCount)}
|
||||
</Text>
|
||||
</IconBadge>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Stack, Group, Text, Loader, Center, Divider, Title, Button } from '@mantine/core';
|
||||
import { Comment } from '~/components/CommentsV2/Comment/Comment';
|
||||
import { RootThreadProvider } from '~/components/CommentsV2/CommentsProvider';
|
||||
import { CreateComment } from '~/components/CommentsV2/Comment/CreateComment';
|
||||
import { IconMessageCancel } from '@tabler/icons-react';
|
||||
import { SortFilter } from '~/components/Filters';
|
||||
import type { ThreadSort } from '~/server/common/enums';
|
||||
import { ReturnToRootThread } from '~/components/CommentsV2/ReturnToRootThread';
|
||||
import classes from '~/components/CommentsV2/Comment/Comment.module.css';
|
||||
import { dialogStore } from '~/components/Dialog/dialogStore';
|
||||
import HiddenCommentsModal from '~/components/CommentsV2/HiddenCommentsModal';
|
||||
|
||||
type Props = {
|
||||
challengeId: number;
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
export function ChallengeDiscussion({ challengeId, userId }: Props) {
|
||||
return (
|
||||
<RootThreadProvider
|
||||
entityType="challenge"
|
||||
entityId={challengeId}
|
||||
limit={10}
|
||||
badges={userId ? [{ userId, label: 'op', color: 'violet' }] : []}
|
||||
>
|
||||
{({
|
||||
data,
|
||||
created,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
showMore,
|
||||
hiddenCount,
|
||||
toggleShowMore,
|
||||
sort,
|
||||
setSort,
|
||||
activeComment,
|
||||
}) => (
|
||||
<Stack mt="xl" gap="xl">
|
||||
<Stack gap={0}>
|
||||
<Group justify="space-between">
|
||||
<Group gap="md">
|
||||
<Title order={2} id="comments">
|
||||
Discussion
|
||||
</Title>
|
||||
{hiddenCount > 0 && !isLoading && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
dialogStore.trigger({
|
||||
component: HiddenCommentsModal,
|
||||
props: { entityId: challengeId, entityType: 'challenge', userId },
|
||||
})
|
||||
}
|
||||
size="compact-xs"
|
||||
>
|
||||
<Group gap={4} justify="center">
|
||||
<IconMessageCancel size={16} />
|
||||
<Text inherit inline>
|
||||
{`See ${hiddenCount} more hidden ${
|
||||
hiddenCount > 1 ? 'comments' : 'comment'
|
||||
}`}
|
||||
</Text>
|
||||
</Group>
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
<SortFilter type="threads" value={sort} onChange={(v) => setSort(v as ThreadSort)} />
|
||||
</Group>
|
||||
<ReturnToRootThread />
|
||||
</Stack>
|
||||
{isLoading || isFetching ? (
|
||||
<Center mt="xl">
|
||||
<Loader type="bars" />
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
{activeComment && (
|
||||
<Stack gap="xl">
|
||||
<Divider />
|
||||
<Text size="sm" c="dimmed">
|
||||
Viewing thread for
|
||||
</Text>
|
||||
<Comment comment={activeComment} viewOnly />
|
||||
</Stack>
|
||||
)}
|
||||
<Stack gap="xl" className={activeComment ? classes.rootCommentReplyInset : undefined}>
|
||||
<CreateComment />
|
||||
<Stack className="relative" gap="xl">
|
||||
{data?.map((comment) => (
|
||||
<Comment key={comment.id} comment={comment} />
|
||||
))}
|
||||
</Stack>
|
||||
{showMore && (
|
||||
<Center>
|
||||
<Button
|
||||
onClick={toggleShowMore}
|
||||
loading={isFetchingNextPage}
|
||||
variant="subtle"
|
||||
size="md"
|
||||
>
|
||||
Load More Comments
|
||||
</Button>
|
||||
</Center>
|
||||
)}
|
||||
{created.map((comment) => (
|
||||
<Comment key={comment.id} comment={comment} />
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</RootThreadProvider>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ type CommentEntityType =
|
||||
| 'article'
|
||||
| 'bounty'
|
||||
| 'bountyEntry'
|
||||
| 'challenge'
|
||||
| 'comment'
|
||||
| 'image';
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ import { MasonryContainer } from '~/components/MasonryColumns/MasonryContainer';
|
||||
import { constants as appConstants } from '~/server/common/constants';
|
||||
import { ImageSort } from '~/server/common/enums';
|
||||
import { CustomMarkdown } from '~/components/Markdown/CustomMarkdown';
|
||||
import { ChallengeDiscussion } from '~/components/Challenge/ChallengeDiscussion';
|
||||
|
||||
/** Open the generation panel for a challenge's model versions. */
|
||||
function openChallengeGenerator(modelVersionIds: number[]) {
|
||||
@@ -409,6 +410,11 @@ function ChallengeDetailsPage({ id }: InferGetServerSidePropsType<typeof getServ
|
||||
{/* Winners Section (for completed challenges) */}
|
||||
{isCompleted && challenge.winners.length > 0 && <ChallengeWinners challenge={challenge} />}
|
||||
|
||||
{/* Discussion Section */}
|
||||
<Container size="xl" id="comments" py={32}>
|
||||
<ChallengeDiscussion challengeId={challenge.id} userId={challenge.createdBy?.id} />
|
||||
</Container>
|
||||
|
||||
{/* Entries Section */}
|
||||
<ChallengeEntries challenge={challenge} />
|
||||
</SensitiveShield>
|
||||
|
||||
@@ -76,6 +76,8 @@ export const upsertCommentV2Handler = async ({
|
||||
? 'BountyEntry'
|
||||
: input.entityType === 'clubPost'
|
||||
? 'ClubPost'
|
||||
: input.entityType === 'challenge'
|
||||
? 'Challenge'
|
||||
: null;
|
||||
|
||||
if (type === 'Post' || type === 'Article') {
|
||||
@@ -119,7 +121,7 @@ export const upsertCommentV2Handler = async ({
|
||||
|
||||
const result = await upsertComment({ ...input, userId: ctx.user.id });
|
||||
if (!input.id) {
|
||||
if (type && type !== 'ClubPost' && type !== 'Article') {
|
||||
if (type && type !== 'ClubPost' && type !== 'Article' && type !== 'Challenge') {
|
||||
await ctx.track.comment({
|
||||
type,
|
||||
nsfw: result.nsfw,
|
||||
@@ -224,19 +226,20 @@ export const toggleHideCommentHandler = async ({
|
||||
const { id, entityType } = input;
|
||||
|
||||
try {
|
||||
const ownerField = entityType === 'challenge' ? 'createdById' : 'userId';
|
||||
const comment = await dbRead.commentV2.findFirst({
|
||||
where: { id },
|
||||
select: {
|
||||
hidden: true,
|
||||
userId: true,
|
||||
thread: { select: { [entityType]: { select: { userId: true } } } },
|
||||
thread: { select: { [entityType]: { select: { [ownerField]: true } } } },
|
||||
},
|
||||
});
|
||||
if (!comment) throw throwNotFoundError(`No comment with id ${input.id}`);
|
||||
if (
|
||||
!isModerator &&
|
||||
// Nasty hack to get around the fact that the thread is not typed
|
||||
(comment.thread[entityType] as any)?.userId !== userId
|
||||
(comment.thread[entityType] as any)?.[ownerField] !== userId
|
||||
)
|
||||
throw throwAuthorizationError();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const threadUrlMap = ({ threadType, threadParentId, ...details }: any) =>
|
||||
review: `/reviews/${threadParentId}?${queryString}`,
|
||||
bounty: `/bounties/${threadParentId}?${queryString}#comments`,
|
||||
bountyEntry: `/bounties/entries/${threadParentId}?${queryString}#comments`,
|
||||
challenge: `/challenges/${threadParentId}?${queryString}#comments`,
|
||||
// question: `/questions/${threadParentId}?highlight=${details.commentId}#comments`,
|
||||
// answer: `/questions/${threadParentId}?highlight=${details.commentId}#answer-`,
|
||||
}[threadType as string] as string;
|
||||
@@ -164,7 +165,8 @@ export const commentNotifications = createNotificationProcessor({
|
||||
root."reviewId",
|
||||
root."articleId",
|
||||
root."bountyId",
|
||||
root."bountyEntryId"
|
||||
root."bountyEntryId",
|
||||
root."challengeId"
|
||||
),
|
||||
'threadType', CASE
|
||||
WHEN root."imageId" IS NOT NULL THEN 'image'
|
||||
@@ -176,6 +178,7 @@ export const commentNotifications = createNotificationProcessor({
|
||||
WHEN root."articleId" IS NOT NULL THEN 'article'
|
||||
WHEN root."bountyId" IS NOT NULL THEN 'bounty'
|
||||
WHEN root."bountyEntryId" IS NOT NULL THEN 'bountyEntry'
|
||||
WHEN root."challengeId" IS NOT NULL THEN 'challenge'
|
||||
ELSE 'comment'
|
||||
END,
|
||||
'commentParentId', t."commentId",
|
||||
@@ -253,6 +256,7 @@ export const commentNotifications = createNotificationProcessor({
|
||||
root."articleId",
|
||||
root."bountyId",
|
||||
root."bountyEntryId",
|
||||
root."challengeId",
|
||||
t."imageId",
|
||||
t."modelId",
|
||||
t."postId",
|
||||
@@ -261,7 +265,8 @@ export const commentNotifications = createNotificationProcessor({
|
||||
t."reviewId",
|
||||
t."articleId",
|
||||
t."bountyId",
|
||||
t."bountyEntryId"
|
||||
t."bountyEntryId",
|
||||
t."challengeId"
|
||||
),
|
||||
'threadType', CASE
|
||||
WHEN COALESCE(root."imageId", t."imageId") IS NOT NULL THEN 'image'
|
||||
@@ -273,6 +278,7 @@ export const commentNotifications = createNotificationProcessor({
|
||||
WHEN COALESCE(root."articleId", t."articleId") IS NOT NULL THEN 'article'
|
||||
WHEN COALESCE(root."bountyId", t."bountyId") IS NOT NULL THEN 'bounty'
|
||||
WHEN COALESCE(root."bountyEntryId", t."bountyEntryId") IS NOT NULL THEN 'bountyEntry'
|
||||
WHEN COALESCE(root."challengeId", t."challengeId") IS NOT NULL THEN 'challenge'
|
||||
ELSE 'comment'
|
||||
END,
|
||||
'commentParentId', COALESCE(
|
||||
@@ -285,6 +291,7 @@ export const commentNotifications = createNotificationProcessor({
|
||||
t."articleId",
|
||||
t."bountyId",
|
||||
t."bountyEntryId",
|
||||
t."challengeId",
|
||||
t."commentId"
|
||||
),
|
||||
'commentParentType', CASE
|
||||
@@ -297,6 +304,7 @@ export const commentNotifications = createNotificationProcessor({
|
||||
WHEN t."articleId" IS NOT NULL THEN 'article'
|
||||
WHEN t."bountyId" IS NOT NULL THEN 'bounty'
|
||||
WHEN t."bountyEntryId" IS NOT NULL THEN 'bountyEntry'
|
||||
WHEN t."challengeId" IS NOT NULL THEN 'challenge'
|
||||
ELSE 'comment'
|
||||
END,
|
||||
'username', u.username
|
||||
@@ -513,4 +521,40 @@ export const commentNotifications = createNotificationProcessor({
|
||||
NOT EXISTS (SELECT 1 FROM "UserNotificationSettings" WHERE "userId" = "ownerId" AND type = 'new-bounty-comment');
|
||||
`,
|
||||
},
|
||||
'new-challenge-comment': {
|
||||
displayName: 'New comments on your challenges',
|
||||
category: NotificationCategory.Comment,
|
||||
prepareMessage: ({ details }) => ({
|
||||
message: `${details.username} commented on your challenge: "${details.challengeTitle}"`,
|
||||
url: `/challenges/${details.challengeId}?highlight=${details.commentId}#comments`,
|
||||
}),
|
||||
prepareQuery: ({ lastSent }) => `
|
||||
WITH new_challenge_comment AS (
|
||||
SELECT DISTINCT
|
||||
ch."createdById" "ownerId",
|
||||
JSONB_BUILD_OBJECT(
|
||||
'version', 2,
|
||||
'challengeId', ch.id,
|
||||
'challengeTitle', ch.title,
|
||||
'commentId', c.id,
|
||||
'username', u.username
|
||||
) as "details"
|
||||
FROM "CommentV2" c
|
||||
JOIN "User" u ON c."userId" = u.id
|
||||
JOIN "Thread" t ON t.id = c."threadId" AND t."challengeId" IS NOT NULL
|
||||
JOIN "Challenge" ch ON ch.id = t."challengeId"
|
||||
WHERE ch."createdById" > 0
|
||||
AND c."createdAt" > '${lastSent}'
|
||||
AND c."userId" != ch."createdById"
|
||||
)
|
||||
SELECT
|
||||
concat('new-comment-challenge:owner:v2:', details->>'commentId') "key",
|
||||
"ownerId" "userId",
|
||||
'new-challenge-comment' "type",
|
||||
details
|
||||
FROM new_challenge_comment
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM "UserNotificationSettings" WHERE "userId" = "ownerId" AND type = 'new-challenge-comment');
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export type ChallengeListItem = {
|
||||
source: ChallengeSource;
|
||||
prizePool: number;
|
||||
entryCount: number;
|
||||
commentCount: number;
|
||||
modelVersionIds: number[];
|
||||
collectionId: number | null;
|
||||
createdBy: {
|
||||
@@ -117,6 +118,7 @@ export type ChallengeDetail = {
|
||||
prizePool: number;
|
||||
operationBudget: number;
|
||||
entryCount: number;
|
||||
commentCount: number;
|
||||
createdBy: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
|
||||
@@ -18,6 +18,7 @@ export const commentConnectorSchema = z.object({
|
||||
'bounty',
|
||||
'bountyEntry',
|
||||
'clubPost',
|
||||
'challenge',
|
||||
]),
|
||||
hidden: z.boolean().nullish(),
|
||||
parentThreadId: z.number().optional(),
|
||||
@@ -54,6 +55,7 @@ export const toggleHideCommentSchema = z.object({
|
||||
'bounty',
|
||||
'bountyEntry',
|
||||
'clubPost',
|
||||
'challenge',
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
@@ -293,6 +293,7 @@ export async function getInfiniteChallenges(input: GetInfiniteChallengesInput) {
|
||||
source: ChallengeSource;
|
||||
prizePool: number;
|
||||
entryCount: bigint;
|
||||
commentCount: bigint;
|
||||
modelVersionIds: number[] | null;
|
||||
modelId: number | null;
|
||||
modelName: string | null;
|
||||
@@ -319,6 +320,7 @@ export async function getInfiniteChallenges(input: GetInfiniteChallengesInput) {
|
||||
c.source,
|
||||
c."prizePool",
|
||||
(SELECT COUNT(*) FROM "CollectionItem" WHERE "collectionId" = c."collectionId" AND status = 'ACCEPTED') as "entryCount",
|
||||
COALESCE((SELECT t."commentCount" FROM "Thread" t WHERE t."challengeId" = c.id), 0) as "commentCount",
|
||||
c."modelVersionIds",
|
||||
(SELECT mv."modelId" FROM "ModelVersion" mv WHERE mv.id = c."modelVersionIds"[1] LIMIT 1) as "modelId",
|
||||
(SELECT m.name FROM "ModelVersion" mv JOIN "Model" m ON m.id = mv."modelId" WHERE mv.id = c."modelVersionIds"[1] LIMIT 1) as "modelName",
|
||||
@@ -384,6 +386,7 @@ export async function getInfiniteChallenges(input: GetInfiniteChallengesInput) {
|
||||
prizePool: item.prizePool,
|
||||
collectionId: item.collectionId,
|
||||
entryCount: Number(item.entryCount),
|
||||
commentCount: Number(item.commentCount),
|
||||
coverImage: coverImage
|
||||
? {
|
||||
id: coverImage.id,
|
||||
@@ -452,6 +455,13 @@ export async function getChallengeDetail(
|
||||
entryCount = Number(countResult.count);
|
||||
}
|
||||
|
||||
// Get comment count from the challenge's thread
|
||||
const commentThread = await dbRead.thread.findUnique({
|
||||
where: { challengeId: id },
|
||||
select: { commentCount: true },
|
||||
});
|
||||
const commentCount = commentThread?.commentCount ?? 0;
|
||||
|
||||
// Get creator info with profile picture and cosmetics
|
||||
const [creator] = await dbRead.$queryRaw<
|
||||
[{ id: number; username: string | null; image: string | null; deletedAt: Date | null }]
|
||||
@@ -627,6 +637,7 @@ export async function getChallengeDetail(
|
||||
prizePool: challenge.prizePool,
|
||||
operationBudget: challenge.operationBudget,
|
||||
entryCount,
|
||||
commentCount,
|
||||
createdBy: {
|
||||
...displayUser,
|
||||
profilePicture: displayProfilePics[displayUserId] ?? null,
|
||||
@@ -1476,6 +1487,18 @@ export async function getActiveEvents(): Promise<ChallengeEventListItem[]> {
|
||||
for (const row of counts) entryCounts.set(row.collectionId, Number(row.count));
|
||||
}
|
||||
|
||||
// Get comment counts for all challenges
|
||||
const allChallengeIds = allChallenges.map((c) => c.id);
|
||||
const commentCounts = new Map<number, number>();
|
||||
if (allChallengeIds.length > 0) {
|
||||
const counts = await dbRead.$queryRaw<
|
||||
Array<{ challengeId: number; commentCount: number }>
|
||||
>`SELECT "challengeId", "commentCount" FROM "Thread" WHERE "challengeId" IN (${Prisma.join(
|
||||
allChallengeIds
|
||||
)})`;
|
||||
for (const row of counts) commentCounts.set(row.challengeId, row.commentCount);
|
||||
}
|
||||
|
||||
return events.map((event) => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
@@ -1500,6 +1523,7 @@ export async function getActiveEvents(): Promise<ChallengeEventListItem[]> {
|
||||
prizePool: c.prizePool,
|
||||
collectionId: c.collectionId,
|
||||
entryCount: c.collectionId ? entryCounts.get(c.collectionId) ?? 0 : 0,
|
||||
commentCount: commentCounts.get(c.id) ?? 0,
|
||||
coverImage: coverImage
|
||||
? {
|
||||
id: coverImage.id,
|
||||
|
||||
@@ -1658,6 +1658,8 @@ export interface Thread {
|
||||
bountyEntry?: BountyEntry | null;
|
||||
clubPostId: number | null;
|
||||
clubPost?: ClubPost | null;
|
||||
challengeId: number | null;
|
||||
challenge?: Challenge | null;
|
||||
metadata: JsonValue;
|
||||
commentCount: number;
|
||||
comments?: CommentV2[];
|
||||
@@ -2835,6 +2837,7 @@ export interface Challenge {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
winners?: ChallengeWinner[];
|
||||
threads?: Thread[];
|
||||
eventId: number | null;
|
||||
event?: ChallengeEvent | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user