[web] profile/activity: add community contributions tab in profile activity page (#1695)

This commit is contained in:
Nitesh Seram 2025-09-15 09:43:42 +05:30 committed by GitHub
parent c21872b653
commit 122ba748d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 275 additions and 2 deletions

View File

@ -117,7 +117,7 @@ export default function InterviewsActivityList({
)}
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between gap-2">
<div className="flex flex-col justify-between gap-x-2 gap-y-3 sm:flex-row sm:items-center">
<Text color="secondary" size="body3">
{intl.formatMessage(
{

View File

@ -8,6 +8,7 @@ import { useIntl } from '~/components/intl';
import TabsUnderline from '~/components/ui/Tabs/TabsUnderline';
import ProfileActivityBookmarkedQuestions from './activity/ProfileActivityBookmarkedQuestions';
import ProfileActivityCommunityContributions from './activity/ProfileActivityCommunityContributions';
import ProfileActivityCompletedQuestions from './activity/ProfileActivityCompletedQuestions';
import ProfileActivityNotifications from './activity/ProfileActivityNotifications';
@ -18,7 +19,7 @@ export default function ProfileActivity() {
const unreadCount = useInterviewsNotificationUnreadCount();
const [selectedTab, setSelectedTab] = useState<
'bookmarked' | 'completed' | 'notifications'
'bookmarked' | 'completed' | 'contributions' | 'notifications'
>(tabParam === 'notifications' ? 'notifications' : 'bookmarked');
function renderTabContent() {
@ -28,6 +29,8 @@ export default function ProfileActivity() {
return <ProfileActivityBookmarkedQuestions />;
case 'completed':
return <ProfileActivityCompletedQuestions />;
case 'contributions':
return <ProfileActivityCommunityContributions />;
case 'notifications':
return <ProfileActivityNotifications />;
default:
@ -56,6 +59,14 @@ export default function ProfileActivity() {
}),
value: 'completed',
},
{
label: intl.formatMessage({
defaultMessage: 'Community contributions',
description: 'Tab label for community contributions',
id: 'xfkHFf',
}),
value: 'contributions',
},
{
label:
unreadCount > 0

View File

@ -0,0 +1,92 @@
import clsx from 'clsx';
import { RiArrowRightLine } from 'react-icons/ri';
import { trpc } from '~/hooks/trpc';
import usePagination from '~/hooks/usePagination';
import InterviewsActivityList from '~/components/interviews/activity/InterviewsActivityList';
import { useIntl } from '~/components/intl';
import Button from '~/components/ui/Button';
import EmptyState from '~/components/ui/EmptyState';
import Spinner from '~/components/ui/Spinner';
const ITEMS_PER_PAGE = 10;
export default function ProfileActivityCommunityContributions() {
const intl = useIntl();
// Pagination
const { currentPage, setCurrentPage } = usePagination({
deps: [],
itemsPerPage: ITEMS_PER_PAGE,
page: 1,
});
const { data, isLoading } = trpc.communityContributions.list.useQuery(
{
pagination: {
limit: ITEMS_PER_PAGE,
page: currentPage,
},
},
{
keepPreviousData: true,
},
);
if (isLoading && data == null) {
return (
<div className="py-10">
<Spinner display="block" />
</div>
);
}
if (data?.contributions?.length === 0 || data == null) {
return (
<div
className={clsx('flex flex-col items-center justify-center', 'h-80')}>
<div className="max-w-[312px]">
<EmptyState
subtitle={intl.formatMessage({
defaultMessage:
'Start sharing your thoughts or solutions — your contributions will appear here.',
description: 'Subtitle for empty state when no contributions',
id: 'u1CNz+',
})}
title={intl.formatMessage({
defaultMessage: 'No community activity yet',
description: 'Title for empty state when no contributions',
id: 'iSFz8P',
})}
variant="empty"
verticalPadding={false}
/>
</div>
<Button
className="mt-6"
href="/interviews/dashboard"
icon={RiArrowRightLine}
label={intl.formatMessage({
defaultMessage: 'Dashboard',
description: 'Link to dashboard page',
id: 'vi10y1',
})}
size="sm"
variant="primary"
/>
</div>
);
}
const { contributions, totalCount } = data;
return (
<InterviewsActivityList
activities={contributions}
currentPage={currentPage}
itemsPerPage={ITEMS_PER_PAGE}
setCurrentPage={setCurrentPage}
totalCount={totalCount}
type="contributions"
/>
);
}

View File

@ -1,5 +1,6 @@
import { router } from '../trpc';
import { authRouter } from './auth';
import { communityContributionsRouter } from './community-contributions';
import { devRouter } from './dev';
import { emailsRouter } from './emails';
import { feedbackRouter } from './feedback';
@ -24,6 +25,7 @@ import { sponsorsRouter } from './sponsors';
export const appRouter = router({
auth: authRouter,
bookmark: questionBookmarkRouter,
communityContributions: communityContributionsRouter,
dev: devRouter,
emails: emailsRouter,
feedback: feedbackRouter,

View File

@ -0,0 +1,168 @@
import { z } from 'zod';
import type {
InterviewsCommentActivity,
InterviewsUpvoteActivity,
} from '~/components/interviews/activity/types';
import { fetchQuestion } from '~/db/QuestionsListReader';
import { unhashQuestion } from '~/db/QuestionsUtils';
import prisma from '~/server/prisma';
import { router, userProcedure } from '../trpc';
export const communityContributionsRouter = router({
list: userProcedure
.input(
z.object({
pagination: z.object({
limit: z
.number()
.int()
.positive()
.transform((val) => Math.min(30, val)),
page: z.number().int().positive(),
}),
}),
)
.query(async ({ ctx: { viewer }, input: { pagination } }) => {
const { limit, page } = pagination;
const [totalCount, contributions] = await Promise.all([
prisma.interviewsActivity.count({
where: {
actorId: viewer.id,
},
}),
prisma.interviewsActivity.findMany({
include: {
actor: {
select: {
avatarUrl: true,
id: true,
name: true,
username: true,
},
},
comment: {
include: {
author: {
select: {
id: true,
name: true,
username: true,
},
},
parentComment: {
include: {
author: {
select: {
id: true,
name: true,
username: true,
},
},
},
},
repliedTo: {
include: {
author: {
select: {
id: true,
name: true,
username: true,
},
},
},
},
},
},
vote: {
include: {
comment: {
include: {
author: {
select: {
id: true,
name: true,
username: true,
},
},
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
skip: (page - 1) * limit,
take: limit,
where: {
actorId: viewer.id,
},
}),
]);
if (contributions.length === 0) {
return {
contributions: [],
totalCount: 0,
};
}
const finalContributions = await Promise.all(
contributions.map(async (contribution) => {
if (
contribution.category === 'DISCUSSION_UPVOTE' &&
contribution.vote
) {
const [format, slug] = unhashQuestion(
contribution.vote.comment.entityId,
);
const { question: questionMetadata } = await fetchQuestion({
format,
slug,
});
return {
...contribution,
question: {
format: questionMetadata.format,
href: questionMetadata.href,
slug: questionMetadata.slug,
title: questionMetadata.title,
},
} as InterviewsUpvoteActivity;
}
if (contribution.category === 'DISCUSSION' && contribution.comment) {
const [format, slug] = unhashQuestion(
contribution.comment.entityId,
);
const { question: questionMetadata } = await fetchQuestion({
format,
slug,
});
return {
...contribution,
question: {
format: questionMetadata.format,
href: questionMetadata.href,
slug: questionMetadata.slug,
title: questionMetadata.title,
},
} as InterviewsCommentActivity;
}
return null;
}),
);
return {
contributions: finalContributions.flatMap((contribution) =>
contribution != null ? [contribution] : [],
),
totalCount,
};
}),
});