diff --git a/js/src/app/user/[userId]/submissions/_components/UserSubmissions/DateRangeIndicator.tsx b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/DateRangeIndicator.tsx new file mode 100644 index 000000000..1b85de066 --- /dev/null +++ b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/DateRangeIndicator.tsx @@ -0,0 +1,66 @@ +import { Box, Tooltip } from "@mantine/core"; +import { IconClock } from "@tabler/icons-react"; + +type DateValue = Date | string | null | undefined; + +interface DateRangeIndicatorProps { + readonly startDate: DateValue; + readonly endDate: DateValue; +} + +function formatDateRange(startDate: DateValue, endDate: DateValue): string { + const fmt = (d: Date | string) => + new Date(d).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + }); + + if (startDate && endDate) + return `Filtered: ${fmt(startDate)} – ${fmt(endDate)}`; + if (startDate) return `Filtered: From ${fmt(startDate)}`; + if (endDate) return `Filtered: Until ${fmt(endDate)}`; + return ""; +} + +export default function DateRangeIndicator({ + startDate, + endDate, +}: Readonly) { + const isActive = !!startDate || !!endDate; + + if (!isActive) return null; + + const label = formatDateRange(startDate, endDate); + + return ( + + + + + + ); +} diff --git a/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.test.tsx b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.test.tsx index 4e97c7caf..fc0021e14 100644 --- a/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.test.tsx +++ b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.test.tsx @@ -99,33 +99,49 @@ const userSubmissionsSuccessHandler = http.get( }), ); -const userSubmissionsEmptyHandler = http.get( - userSubmissionsUrl.url.toString(), - () => - HttpResponse.json({ - success: true, - message: "Submissions loaded!", - payload: { - hasNextPage: false, - pages: 1, - items: [], - }, - }), -); - const userSubmissionsErrorHandler = http.get( userSubmissionsUrl.url.toString(), () => HttpResponse.error(), ); +const mockUseUserSubmissionsQuery = vi.fn(); + +vi.mock("@/lib/api/queries/user", () => ({ + useUserSubmissionsQuery: (...args: unknown[]) => + mockUseUserSubmissionsQuery(...args), +})); + +const BASE_QUERY_RESULT = { + status: "pending", + page: 1, + goBack: vi.fn(), + goForward: vi.fn(), + isPlaceholderData: false, + goTo: vi.fn(), + searchQuery: "", + setSearchQuery: vi.fn(), + pointFilter: false, + togglePointFilter: vi.fn(), + topics: [], + setTopics: vi.fn(), + clearTopics: vi.fn(), + startDate: undefined, + endDate: undefined, + setStartDate: vi.fn(), + setEndDate: vi.fn(), + data: undefined, +}; + describe("UserSubmissions succeeded", () => { afterEach(() => { cleanup(); + vi.clearAllMocks(); }); let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null; beforeEach(() => { renderProviderFn = TestUtils.getRenderWithAllProvidersFn(); + mockUseUserSubmissionsQuery.mockReturnValue(BASE_QUERY_RESULT); }); it("should render skeleton stack of submissions initially", () => { @@ -137,6 +153,54 @@ describe("UserSubmissions succeeded", () => { expect(element).toBeInTheDocument(); expect(element).toBeVisible(); }); + + it("should not render DateRangeIndicator when no date range is set", () => { + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + startDate: undefined, + endDate: undefined, + }); + renderProviderFn?.(); + expect( + screen.queryByTestId("date-range-indicator"), + ).not.toBeInTheDocument(); + }); + + it("should render DateRangeIndicator when startDate is set", () => { + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "success", + data: { payload: { items: [], pages: 0, hasNextPage: false } }, + startDate: "2026-01-01", + endDate: undefined, + }); + renderProviderFn?.(); + expect(screen.getByTestId("date-range-indicator")).toBeInTheDocument(); + }); + + it("should render DateRangeIndicator when endDate is set", () => { + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "success", + data: { payload: { items: [], pages: 0, hasNextPage: false } }, + startDate: undefined, + endDate: "2026-03-01", + }); + renderProviderFn?.(); + expect(screen.getByTestId("date-range-indicator")).toBeInTheDocument(); + }); + + it("should render DateRangeIndicator when both dates are set", () => { + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "success", + data: { payload: { items: [], pages: 0, hasNextPage: false } }, + startDate: "2026-01-01", + endDate: "2026-03-01", + }); + renderProviderFn?.(); + expect(screen.getByTestId("date-range-indicator")).toBeInTheDocument(); + }); }); describe("UserSubmissions with successful API", () => { @@ -146,12 +210,25 @@ describe("UserSubmissions with successful API", () => { afterEach(() => { server.resetHandlers(); cleanup(); + vi.clearAllMocks(); }); afterAll(() => server.close()); let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null; beforeEach(() => { renderProviderFn = TestUtils.getRenderWithAllProvidersFn(); + + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "success", + data: { + payload: { + items: MOCK_SUBMISSIONS, + pages: 1, + hasNextPage: false, + }, + }, + }); }); it("should render submission titles after successful API call", async () => { @@ -219,7 +296,11 @@ describe("UserSubmissions with successful API", () => { }); it("should show Nothing found when empty", async () => { - server.use(userSubmissionsEmptyHandler); + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "success", + data: { payload: { items: [], pages: 1, hasNextPage: false } }, + }); renderProviderFn?.(); await waitFor(() => { @@ -235,12 +316,18 @@ describe("UserSubmissions error state", () => { afterEach(() => { server.resetHandlers(); cleanup(); + vi.clearAllMocks(); }); afterAll(() => server.close()); let renderProviderFn: TestUtilTypes.RenderWithAllProvidersFn | null = null; beforeEach(() => { renderProviderFn = TestUtils.getRenderWithAllProvidersFn(); + mockUseUserSubmissionsQuery.mockReturnValue({ + ...BASE_QUERY_RESULT, + status: "error", + data: undefined, + }); }); it("should show error toast when API errors", async () => { diff --git a/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.tsx b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.tsx index d29e1a0cb..385cea91c 100644 --- a/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.tsx +++ b/js/src/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissions.tsx @@ -1,5 +1,6 @@ import DateRangePopover from "@/app/user/[userId]/submissions/_components/DateRangePopover/DateRangePopover"; import TopicFilterPopover from "@/app/user/[userId]/submissions/_components/TopicFilters/TopicFilterPopover"; +import DateRangeIndicator from "@/app/user/[userId]/submissions/_components/UserSubmissions/DateRangeIndicator"; import UserSubmissionsSkeleton from "@/app/user/[userId]/submissions/_components/UserSubmissions/UserSubmissionsSkeleton"; import CodebloomCard from "@/components/ui/CodebloomCard"; import FilterDropdown from "@/components/ui/dropdown/FilterDropdown"; @@ -85,6 +86,36 @@ export default function UserSubmissions({ userId }: { userId: string }) { const pageData = data.payload; + const filterDropdown = ( + + + + + Points Received + + } + /> + + + + + ); + return ( {!isMobile && ( - - - - Points Received - - } - /> - - + {filterDropdown} )} - {isMobile && ( - - - - Points Received - - } - /> - - - )} + {isMobile && filterDropdown} {isPlaceholderData && (