Skip to content
Merged
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
Expand Up @@ -14,7 +14,7 @@
}

.small {
flex: 1 1 0;
flex: 0.5 1 0;
}

.fluid {
Expand Down
159 changes: 147 additions & 12 deletions frontend/src/components/admin-search-bar/admin-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import clsx from "clsx";
import { useState } from "react";
import {
addDays,
addMonths,
addWeeks,
addYears,
format,
subDays,
subMonths,
subWeeks,
subYears,
} from "date-fns";
import { useState, useMemo } from "react";
import { Icon, Input, Dropdown, DropdownProps } from "semantic-ui-react";

import styles from "./admin-search-bar.module.scss";
Expand All @@ -8,6 +19,7 @@ export type Filters = {
title: string;
venue: string;
date: string;
status: string;
};

type Props = {
Expand All @@ -31,11 +43,35 @@ const venueOptions = [
{ key: "tr3", text: "Theme Room 3", value: "Theme Room 3" },
];

const statusOptions = [
{ key: "app", text: "Approved", value: "Approved" },
{ key: "pen", text: "Pending", value: "Pending" },
{ key: "rej", text: "Rejected", value: "Rejected" }
];

const dateOptions = [
{ key: "tomorrow", text: "Tomorrow", value: "tomorrow" },
{ key: "next3days", text: "Next 3 days", value: "next3days" },
{ key: "nextweek", text: "Next week", value: "nextweek" },
{ key: "next2weeks", text: "Next 2 weeks", value: "next2weeks" },
{ key: "nextmonth", text: "Next month", value: "nextmonth" },
{ key: "next3months", text: "Next 3 months", value: "next3months" },
{ key: "nextyear", text: "Next year", value: "nextyear" },
{ key: "yesterday", text: "Yesterday", value: "yesterday" },
{ key: "last3days", text: "Last 3 days", value: "last3days" },
{ key: "lastweek", text: "Last week", value: "lastweek" },
{ key: "last2weeks", text: "Last 2 weeks", value: "last2weeks" },
{ key: "lastmonth", text: "Last month", value: "lastmonth" },
{ key: "last3months", text: "Last 3 months", value: "last3months" },
{ key: "lastyear", text: "Last year", value: "lastyear" },
];

function SearchBar({ className, onFilterChange, fluid = false }: Props) {
const [filters, setFilters] = useState<Filters>({
title: "",
venue: "",
date: "",
status: ""
});

const setField = (field: keyof Filters, value: string) => {
Expand All @@ -51,6 +87,98 @@ function SearchBar({ className, onFilterChange, fluid = false }: Props) {
setField("venue", data.value as string);
};

const handleStatusChange = (
_event: React.SyntheticEvent<HTMLElement>,
data: DropdownProps
) => {
setField("status", data.value as string);
};

const handleDateChange = (
_event: React.SyntheticEvent<HTMLElement>,
data: DropdownProps,
) => {
const value = data.value as string;

if (!value) {
setField("date", "");
return;
}

const today = new Date();
let newDate = today;

switch (value) {
case "tomorrow":
newDate = addDays(today, 1);
break;
case "next3days":
newDate = addDays(today, 3);
break;
case "nextweek":
newDate = addWeeks(today, 1);
break;
case "next2weeks":
newDate = addWeeks(today, 2);
break;
case "nextmonth":
newDate = addMonths(today, 1);
break;
case "next3months":
newDate = addMonths(today, 3);
break;
case "nextyear":
newDate = addYears(today, 1);
break;
case "yesterday":
newDate = subDays(today, 1);
break;
case "last3days":
newDate = subDays(today, 3);
break;
case "lastweek":
newDate = subWeeks(today, 1);
break;
case "last2weeks":
newDate = subWeeks(today, 2);
break;
case "lastmonth":
newDate = subMonths(today, 1);
break;
case "last3months":
newDate = subMonths(today, 3);
break;
case "lastyear":
newDate = subYears(today, 1);
break;
}

setField("date", format(newDate, "yyyy-MM-dd"));
};

const currentShortcutValue = useMemo(() => {
if (!filters.date) return "";
const today = new Date();
const target = filters.date;

if (target === format(addDays(today, 1), "yyyy-MM-dd")) return "tomorrow";
if (target === format(addDays(today, 3), "yyyy-MM-dd")) return "next3days";
if (target === format(addWeeks(today, 1), "yyyy-MM-dd")) return "nextweek";
if (target === format(addWeeks(today, 2), "yyyy-MM-dd")) return "next2weeks";
if (target === format(addMonths(today, 1), "yyyy-MM-dd")) return "nextmonth";
if (target === format(addMonths(today, 3), "yyyy-MM-dd")) return "next3months";
if (target === format(addYears(today, 1), "yyyy-MM-dd")) return "nextyear";
if (target === format(subDays(today, 1), "yyyy-MM-dd")) return "yesterday";
if (target === format(subDays(today, 3), "yyyy-MM-dd")) return "last3days";
if (target === format(subWeeks(today, 1), "yyyy-MM-dd")) return "lastweek";
if (target === format(subWeeks(today, 2), "yyyy-MM-dd")) return "last2weeks";
if (target === format(subMonths(today, 1), "yyyy-MM-dd")) return "lastmonth";
if (target === format(subMonths(today, 3), "yyyy-MM-dd")) return "last3months";
if (target === format(subYears(today, 1), "yyyy-MM-dd")) return "lastyear";

return "";
}, [filters.date]);
Comment on lines +159 to +180
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currentShortcutValue computation performs multiple date calculations and format operations on every render. While it's memoized with useMemo, the calculation itself is still expensive with 14 conditional branches. Consider extracting this logic into a helper function or simplifying by storing the shortcut value directly in state when a shortcut is selected, rather than reverse-engineering it from the date string.

Copilot uses AI. Check for mistakes.

return (
<div className={clsx(styles.container, fluid && styles.fluid, className)}>
<Input
Expand Down Expand Up @@ -81,20 +209,27 @@ function SearchBar({ className, onFilterChange, fluid = false }: Props) {
onChange={handleVenueChange}
/>

<Input
<Dropdown
fluid
selection
clearable
className={clsx(styles.input, styles.small)}
icon={
filters.date ? (
<Icon name="times" link onClick={() => setField("date", "")} />
) : (
<Icon name="calendar alternate outline" />
)
}
iconPosition="left"
input={{ type: "date", value: filters.date }}
onChange={(_, { value }) => setField("date", value)}
placeholder="Date"
options={dateOptions}
value={currentShortcutValue}
onChange={handleDateChange}
/>

<Dropdown
fluid
selection
search
clearable
className={clsx(styles.input, styles.small)}
placeholder="Status"
options={statusOptions}
value={filters.status}
onChange={handleStatusChange}
/>
</div>
);
Expand Down
57 changes: 40 additions & 17 deletions frontend/src/custom-hooks/use-table-state-admin.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { endOfDay, isWithinInterval, parseISO, startOfDay } from "date-fns";
import throttle from "lodash/throttle";
import { Key, useMemo, useState } from "react";
import { SortOrder } from "react-base-table";
Expand All @@ -14,7 +15,17 @@ export type TableStateOptions = {
defaultSortBy?: SortBy;
};

export default function useTableState<T>(
interface FilterableItem {
title?: string;
venue?: {
name?: string;
} | null;
eventDateString?: string;
startDateTime?: number | string;
status?: string;
}

export default function useTableState<T extends FilterableItem>(
data: T[],
{ defaultSortBy }: TableStateOptions = {},
) {
Expand All @@ -23,6 +34,7 @@ export default function useTableState<T>(
title: "",
venue: "",
date: "",
status: ""
});

const onFilterChange = useMemo(
Expand All @@ -31,24 +43,17 @@ export default function useTableState<T>(
);

const filteredData = useMemo(() => {
const { title, venue, date } = activeFilters;

const { title, venue, date, status } = activeFilters;
if (
data.length === 0 ||
(!title && !venue && !date)
(!title && !venue && !date && !status)
) {
return data;
}

const formattedDate = date
? new Date(date).toLocaleDateString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})
: "";

return data.filter((item: any) => {
return data.filter((datum) => {
const item = datum;
Comment on lines +54 to +55
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable item is assigned from datum without any transformation or purpose. This is redundant and adds no value. Either use datum directly throughout the filter function or remove this assignment.

Suggested change
return data.filter((datum) => {
const item = datum;
return data.filter((item) => {

Copilot uses AI. Check for mistakes.

const titleMatch =
!title ||
(item.title &&
Expand All @@ -59,11 +64,29 @@ export default function useTableState<T>(
(item.venue?.name &&
item.venue.name.toLowerCase().includes(venue.toLowerCase()));

const dateMatch =
!date ||
(item.eventDateString && item.eventDateString.includes(formattedDate));
const dateMatch = (() => {
if (!date) {
return true;
}

if (!item.startDateTime) {
return false;
}

const filterDate = parseISO(date);
const today = new Date();
const start = startOfDay(filterDate < today ? filterDate : today);
const end = endOfDay(filterDate > today ? filterDate : today);

return isWithinInterval(new Date(item.startDateTime), { start, end });
})();

const statusMatch =
!status ||
(item.status &&
item.status.toLowerCase() === status.toLowerCase());

return titleMatch && venueMatch && dateMatch;
return titleMatch && venueMatch && dateMatch && statusMatch;
});
}, [data, activeFilters]);

Expand Down