Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion js/src/__mock__/user/index.ts
Original file line number Diff line number Diff line change
@@ -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", {
Expand 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: [],
},
{
Expand Down Expand Up @@ -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<typeof userProfileUrl.res>);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
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);

Check warning on line 11 in js/src/app/dashboard/_components/RecentSubmissions/RecentSubmissions.tsx

View check run for this annotation

SonarQubeCloud / [codebloom_frontend] SonarCloud Code Analysis

useState call is not destructured into value + setter pair

See more on https://sonarcloud.io/project/issues?id=Patina-Network_codebloom_frontend&issues=AaGBGCkcmfyg43yDlI-O&open=AaGBGCkcmfyg43yDlI-O&pullRequest=1055

useEffect(() => {
const timer = window.setInterval(() => setTick((tick) => tick + 1), 60_000);
return () => window.clearInterval(timer);
}, []);

if (status === "pending") {
return <RecentSubmissionsSkeleton />;
Expand Down
38 changes: 38 additions & 0 deletions js/src/app/user/[userId]/UserProfile.page.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import("react-router-dom")>()),
useNavigate: () => mockNavigate,
}));

class ResizeObserverMock {
observe() {}
unobserve() {}
Expand Down Expand Up @@ -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");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -34,7 +36,15 @@ export default function UserTags({ userId }: { userId: string }) {

return (
<Stack gap="md" w="100%">
<TagList tags={tags} size={40} gap="xs" expanded={true} />
<TagList
tags={tags}
size={40}
gap="xs"
expanded={true}
onTagClick={(userTag) =>
navigate(`/leaderboard?${userTag.tag.toLowerCase()}=true`)
}
/>
<UserAchievement achievements={achievements} size={40} gap="xs" />
</Stack>
);
Expand Down
46 changes: 36 additions & 10 deletions js/src/components/ui/tags/TagList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,49 @@ 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 {
tags: Api<"UserTag">[];
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({
tags,
size = 20,
gap = "xs",
expanded = false,
}: TagListProps) {
onTagClick,
}: Readonly<TagListProps>) {
const filteredTags = ApiUtils.filterUnusedTags(tags);
const tagItems = useMemo(
() =>
filteredTags.map((userTag) => (
<TagBadge key={userTag.id} userTag={userTag} size={size} />
<TagBadge
key={userTag.id}
userTag={userTag}
size={size}
onTagClick={onTagClick}
/>
)),
[filteredTags, size],
[filteredTags, size, onTagClick],
);

if (!tagFF) return null;
Expand Down Expand Up @@ -58,16 +73,27 @@ export default function TagList({
);
}

function TagBadge({ userTag, size }: TagBadgeProps) {
function TagBadge({ userTag, size, onTagClick }: Readonly<TagBadgeProps>) {
const metadata = ApiUtils.getMetadataByTagEnum(userTag.tag);
const image = (
<Image
src={metadata.icon}
alt={metadata.alt}
style={{ height: size, width: "auto" }}
/>
);

return (
<Tooltip label={metadata.name} color="dark.4" withArrow position="top">
<Image
src={metadata.icon}
alt={metadata.alt}
style={{ height: size, width: "auto", cursor: "pointer" }}
/>
{onTagClick ?
<UnstyledButton
type="button"
aria-label={metadata.name}
onClick={() => onTagClick(userTag)}
>
{image}
</UnstyledButton>
: image}
</Tooltip>
);
}
Loading