From 85037abe8836d21a919ef92b3fdc452148279473 Mon Sep 17 00:00:00 2001 From: Derek Date: Tue, 27 Oct 2026 22:07:21 -0400 Subject: [PATCH 1/4] DINO-1114: profile leaderboard link to open current leaderboard --- .../_components/UserProfile/UserTags/UserTags.tsx | 12 +++++++++++- js/src/components/ui/tags/TagList.tsx | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/js/src/app/user/[userId]/_components/UserProfile/UserTags/UserTags.tsx b/js/src/app/user/[userId]/_components/UserProfile/UserTags/UserTags.tsx index a926cbba8..006163f10 100644 --- a/js/src/app/user/[userId]/_components/UserProfile/UserTags/UserTags.tsx +++ b/js/src/app/user/[userId]/_components/UserProfile/UserTags/UserTags.tsx @@ -5,8 +5,10 @@ import Toast from "@/components/ui/toast/Toast"; import { useUserProfileQuery } from "@/lib/api/queries/user"; import { Group, Text, Stack } from "@mantine/core"; import { IconAlertTriangle } from "@tabler/icons-react"; +import { useNavigate } from "react-router-dom"; export default function UserTags({ userId }: { userId: string }) { + const navigate = useNavigate(); const { data, status } = useUserProfileQuery({ userId }); if (status === "pending") { @@ -34,7 +36,15 @@ export default function UserTags({ userId }: { userId: string }) { return ( - + + navigate(`/leaderboard?${userTag.tag.toLowerCase()}=true`) + } + /> ); diff --git a/js/src/components/ui/tags/TagList.tsx b/js/src/components/ui/tags/TagList.tsx index d317bed79..ee4ea8e4a 100644 --- a/js/src/components/ui/tags/TagList.tsx +++ b/js/src/components/ui/tags/TagList.tsx @@ -10,11 +10,13 @@ interface TagListProps { size?: number; gap?: string | number; expanded?: boolean; + onTagClick?: (userTag: Api<"UserTag">) => void; } interface TagBadgeProps { userTag: Api<"UserTag">; size: number; + onTagClick?: (userTag: Api<"UserTag">) => void; } export default function TagList({ @@ -22,14 +24,20 @@ export default function TagList({ size = 20, gap = "xs", expanded = false, + onTagClick, }: TagListProps) { const filteredTags = ApiUtils.filterUnusedTags(tags); const tagItems = useMemo( () => filteredTags.map((userTag) => ( - + )), - [filteredTags, size], + [filteredTags, size, onTagClick], ); if (!tagFF) return null; @@ -58,7 +66,7 @@ export default function TagList({ ); } -function TagBadge({ userTag, size }: TagBadgeProps) { +function TagBadge({ userTag, size, onTagClick }: TagBadgeProps) { const metadata = ApiUtils.getMetadataByTagEnum(userTag.tag); return ( @@ -67,6 +75,7 @@ function TagBadge({ userTag, size }: TagBadgeProps) { src={metadata.icon} alt={metadata.alt} style={{ height: size, width: "auto", cursor: "pointer" }} + onClick={() => onTagClick?.(userTag)} /> ); From 40e9acad8441d268764646bee83f18fb5c8c11b7 Mon Sep 17 00:00:00 2001 From: Derek Date: Tue, 27 Oct 2026 23:34:11 -0400 Subject: [PATCH 2/4] DINO-1114: add readonly to props --- js/src/components/ui/tags/TagList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/components/ui/tags/TagList.tsx b/js/src/components/ui/tags/TagList.tsx index ee4ea8e4a..e0eab247d 100644 --- a/js/src/components/ui/tags/TagList.tsx +++ b/js/src/components/ui/tags/TagList.tsx @@ -25,7 +25,7 @@ export default function TagList({ gap = "xs", expanded = false, onTagClick, -}: TagListProps) { +}: Readonly) { const filteredTags = ApiUtils.filterUnusedTags(tags); const tagItems = useMemo( () => @@ -66,7 +66,7 @@ export default function TagList({ ); } -function TagBadge({ userTag, size, onTagClick }: TagBadgeProps) { +function TagBadge({ userTag, size, onTagClick }: Readonly) { const metadata = ApiUtils.getMetadataByTagEnum(userTag.tag); return ( From f229812a6e246c9fdd7f8ab4847286fbcc8abbb1 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:56:00 +0000 Subject: [PATCH 3/4] Refresh recent submission times every minute and make clickable tag badges keyboard accessible --- .../RecentSubmissions/RecentSubmissions.tsx | 7 +++++ js/src/components/ui/tags/TagList.tsx | 31 ++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/js/src/app/dashboard/_components/RecentSubmissions/RecentSubmissions.tsx b/js/src/app/dashboard/_components/RecentSubmissions/RecentSubmissions.tsx index cb3f54fa8..b86b87280 100644 --- a/js/src/app/dashboard/_components/RecentSubmissions/RecentSubmissions.tsx +++ b/js/src/app/dashboard/_components/RecentSubmissions/RecentSubmissions.tsx @@ -3,10 +3,17 @@ import CodebloomCard from "@/components/ui/CodebloomCard"; import { useUserSubmissionsQuery } from "@/lib/api/queries/user"; import { timeDiff } from "@/lib/timeDiff"; import { Badge, Box, Button, Center, Flex, Text, Title } from "@mantine/core"; +import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; export default function RecentSubmissions({ userId }: { userId: string }) { const { data, status } = useUserSubmissionsQuery({ userId }); + const [, setTick] = useState(0); + + useEffect(() => { + const timer = window.setInterval(() => setTick((tick) => tick + 1), 60_000); + return () => window.clearInterval(timer); + }, []); if (status === "pending") { return ; diff --git a/js/src/components/ui/tags/TagList.tsx b/js/src/components/ui/tags/TagList.tsx index e0eab247d..a71893c32 100644 --- a/js/src/components/ui/tags/TagList.tsx +++ b/js/src/components/ui/tags/TagList.tsx @@ -2,7 +2,14 @@ import AchievementCarousel from "@/components/ui/carousel/ItemCarousel"; import { Api } from "@/lib/api/types"; import { ApiUtils } from "@/lib/api/utils"; import { tagFF } from "@/lib/ff"; -import { Image, Tooltip, Stack, Divider, Text } from "@mantine/core"; +import { + Image, + Tooltip, + Stack, + Divider, + Text, + UnstyledButton, +} from "@mantine/core"; import { useMemo } from "react"; interface TagListProps { @@ -68,15 +75,25 @@ export default function TagList({ function TagBadge({ userTag, size, onTagClick }: Readonly) { const metadata = ApiUtils.getMetadataByTagEnum(userTag.tag); + const image = ( + {metadata.alt} + ); return ( - {metadata.alt} onTagClick?.(userTag)} - /> + {onTagClick ? + onTagClick(userTag)} + > + {image} + + : image} ); } From 3ed1002abaa8b0cdcb7ed0a2e8a6ccef7ec8fa18 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 28 Oct 2026 18:19:00 -0400 Subject: [PATCH 4/4] DINO-1114: add tag navigation test --- js/src/__mock__/user/index.ts | 28 +++++++++++++- .../user/[userId]/UserProfile.page.test.tsx | 38 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/js/src/__mock__/user/index.ts b/js/src/__mock__/user/index.ts index 21ba87412..65cef5d80 100644 --- a/js/src/__mock__/user/index.ts +++ b/js/src/__mock__/user/index.ts @@ -1,4 +1,5 @@ import { ApiURL } from "@/lib/api/common/apiURL"; +import { Tag } from "@/lib/api/types/schema"; import { http, HttpResponse } from "msw"; const allUsersUrl = ApiURL.create("/api/user/all", { @@ -14,7 +15,14 @@ export const MOCK_USERS = [ nickname: null, admin: false, profileUrl: null, - tags: [], + tags: [ + { + id: "tag-1", + createdAt: "2024-01-01T00:00:00Z", + userId: "user-1", + tag: Tag.Patina, + }, + ], achievements: [], }, { @@ -86,3 +94,21 @@ export const getAllUsersErrorHandler = http.get( return HttpResponse.error(); }, ); + +const userProfileUrl = ApiURL.create("/api/user/{userId}/profile", { + method: "GET", + params: { + userId: MOCK_USERS[0].id, + }, +}); + +export const getUserProfileHandler = http.get( + userProfileUrl.url.toString(), + () => { + return HttpResponse.json({ + success: true, + message: "User profile loaded!", + payload: MOCK_USERS[0], + } satisfies ReturnType); + }, +); diff --git a/js/src/app/user/[userId]/UserProfile.page.test.tsx b/js/src/app/user/[userId]/UserProfile.page.test.tsx index 3ceb6de35..b2feefdce 100644 --- a/js/src/app/user/[userId]/UserProfile.page.test.tsx +++ b/js/src/app/user/[userId]/UserProfile.page.test.tsx @@ -1,9 +1,17 @@ +import { getUserProfileHandler } from "@/__mock__/user"; import UserProfilePage from "@/app/user/[userId]/UserProfile.page"; import { TestUtils, TestUtilTypes } from "@/lib/test"; import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { setupServer } from "msw/node"; import { Route, Routes } from "react-router-dom"; +const mockNavigate = vi.fn(); +vi.mock("react-router-dom", async (importOriginal) => ({ + ...(await importOriginal()), + useNavigate: () => mockNavigate, +})); + class ResizeObserverMock { observe() {} unobserve() {} @@ -137,4 +145,34 @@ describe("UserProfilePage", () => { ); }); }); + + describe("should navigate to leaderboard when a tag is clicked", () => { + const server = setupServer(getUserProfileHandler); + + beforeAll(() => server.listen()); + afterEach(() => { + server.resetHandlers(); + }); + afterAll(() => server.close()); + + let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null; + beforeEach(() => { + renderProviderFn = TestUtils.getRenderWithAllProvidersFn(); + }); + + it("should navigate to leaderboard with tag query param", async () => { + const user = userEvent.setup(); + renderProviderFn?.(routes, "/user/user-1"); + + const tagBadge = await screen.findByRole("button", { + name: "Patina Network", + }); + + expect(tagBadge).toBeInTheDocument(); + + await user.click(tagBadge); + + expect(mockNavigate).toHaveBeenCalledWith("/leaderboard?patina=true"); + }); + }); });