diff --git a/apps/web/src/components/interviews/activity/InterviewsActivityList.tsx b/apps/web/src/components/interviews/activity/InterviewsActivityList.tsx
index 1f68f5914..e9456a186 100644
--- a/apps/web/src/components/interviews/activity/InterviewsActivityList.tsx
+++ b/apps/web/src/components/interviews/activity/InterviewsActivityList.tsx
@@ -117,7 +117,7 @@ export default function InterviewsActivityList({
)}
{totalPages > 1 && (
-
+
{intl.formatMessage(
{
diff --git a/apps/web/src/components/profile/ProfileActivity.tsx b/apps/web/src/components/profile/ProfileActivity.tsx
index 14eb7d487..4dd3f2dc4 100644
--- a/apps/web/src/components/profile/ProfileActivity.tsx
+++ b/apps/web/src/components/profile/ProfileActivity.tsx
@@ -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 ;
case 'completed':
return ;
+ case 'contributions':
+ return ;
case 'notifications':
return ;
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
diff --git a/apps/web/src/components/profile/activity/ProfileActivityCommunityContributions.tsx b/apps/web/src/components/profile/activity/ProfileActivityCommunityContributions.tsx
new file mode 100644
index 000000000..d163e49e6
--- /dev/null
+++ b/apps/web/src/components/profile/activity/ProfileActivityCommunityContributions.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ if (data?.contributions?.length === 0 || data == null) {
+ return (
+
+ );
+ }
+
+ const { contributions, totalCount } = data;
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/server/routers/_app.ts b/apps/web/src/server/routers/_app.ts
index 79a943678..10b0adc59 100644
--- a/apps/web/src/server/routers/_app.ts
+++ b/apps/web/src/server/routers/_app.ts
@@ -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,
diff --git a/apps/web/src/server/routers/community-contributions.ts b/apps/web/src/server/routers/community-contributions.ts
new file mode 100644
index 000000000..450bad585
--- /dev/null
+++ b/apps/web/src/server/routers/community-contributions.ts
@@ -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,
+ };
+ }),
+});