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
Original file line number Diff line number Diff line change
@@ -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<DateRangeIndicatorProps>) {
const isActive = !!startDate || !!endDate;

if (!isActive) return null;

const label = formatDateRange(startDate, endDate);

return (
<Tooltip
label={label}
withArrow
position="top"
events={{ hover: true, focus: false, touch: false }}
>
<Box
data-testid="date-range-indicator"
pos="absolute"
top={-8}
right={-8}
bg="green.8"
c="white"
w={22}
h={22}
display="flex"
style={{
zIndex: 10,
borderRadius: "50%",
border: "2px solid white",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<IconClock size={13} />
</Box>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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?.(<UserSubmissions userId={uuid()} />);
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?.(<UserSubmissions userId={uuid()} />);
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?.(<UserSubmissions userId={uuid()} />);
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?.(<UserSubmissions userId={uuid()} />);
expect(screen.getByTestId("date-range-indicator")).toBeInTheDocument();
});
});

describe("UserSubmissions with successful API", () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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?.(<UserSubmissions userId={FIXED_USER_ID} />);

await waitFor(() => {
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -85,6 +86,36 @@ export default function UserSubmissions({ userId }: { userId: string }) {

const pageData = data.payload;

const filterDropdown = (
<Box pos="relative" display="inline-block">
<FilterDropdown buttonName="Filters">
<TopicFilterPopover
value={topics}
selectedTopicsSet={selectedTopicsSet}
onChange={setTopics}
onClear={clearTopics}
/>
<FilterDropdownItem
value={pointFilter}
toggle={togglePointFilter}
switchMode
name={
<Flex gap="0.5rem" align="center">
Points Received
</Flex>
}
/>
<DateRangePopover
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
</FilterDropdown>
<DateRangeIndicator startDate={startDate} endDate={endDate} />
</Box>
);

return (
<Box
mt={10}
Expand All @@ -96,30 +127,7 @@ export default function UserSubmissions({ userId }: { userId: string }) {
>
{!isMobile && (
<Box display="block" style={{ textAlign: "right" }}>
<FilterDropdown buttonName="Filters">
<TopicFilterPopover
value={topics}
selectedTopicsSet={selectedTopicsSet}
onChange={setTopics}
onClear={clearTopics}
/>
<FilterDropdownItem
value={pointFilter}
toggle={togglePointFilter}
switchMode
name={
<Flex gap="0.5rem" align="center">
Points Received
</Flex>
}
/>
<DateRangePopover
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
</FilterDropdown>
{filterDropdown}
</Box>
)}
<Group
Expand All @@ -138,32 +146,7 @@ export default function UserSubmissions({ userId }: { userId: string }) {
w={isMobile ? "100%" : undefined}
/>
</Box>
{isMobile && (
<FilterDropdown buttonName="Filters">
<TopicFilterPopover
value={topics}
selectedTopicsSet={selectedTopicsSet}
onChange={setTopics}
onClear={clearTopics}
/>
<FilterDropdownItem
value={pointFilter}
toggle={togglePointFilter}
switchMode
name={
<Flex gap="0.5rem" align="center">
Points Received
</Flex>
}
/>
<DateRangePopover
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
</FilterDropdown>
)}
{isMobile && filterDropdown}
</Group>
<Box pos="relative">
{isPlaceholderData && (
Expand Down
Loading