Added a "Published in the last X days" filter, applied by the backend

GET /api/opportunities takes released_from, an RFC 3339 time, and lists
only the opportunities whose latest release is at or after it. It works
with paging and with changed_after, which keeps the same batch either
way; an invalid value is 400.

The frontend's filter defaults to 7 days, from midnight UK time, and
sends it with every load and catch-up, so a year of opportunities isn't
downloaded to show a week's. Changing the days loads the list again;
Clear filters goes back to 7.
This commit is contained in:
bwilliams committed 2026-10-19 10:05:45 +01:00
1 parent b8244da2e5
commit 746fa0a9c1
19 files changed
+333 -64

No files matched your search

+28
View File
@@ -48,6 +48,7 @@ function toggled<T>(set: ReadonlySet<T>, value: T): Set<T> {
*/
export function FilterPanel(props: Props): JSX.Element {
const titleId = createUniqueId();
const daysId = createUniqueId();
const bodyId = createUniqueId();
// Open by default; the last choice is remembered.
@@ -61,6 +62,15 @@ export function FilterPanel(props: Props): JSX.Element {
const update = (patch: Partial<OpportunityFilters>): void =>
props.onChange({ ...props.filters, ...patch });
// Read when the input is committed (Enter or leaving it), not on each
// key, as each change loads the opportunities again. A value that isn't
// a whole number of at least 1 is put back as it was.
const commitDays = (field: HTMLInputElement): void => {
const days = Number(field.value);
if (Number.isInteger(days) && days >= 1) update({ releasedWithinDays: days });
else field.value = String(props.filters.releasedWithinDays);
};
return (
<section class="mb-4 rounded-[10px] border border-line bg-surface" aria-label="Filters">
<div class="flex flex-wrap items-center gap-x-5 gap-y-2 px-3 py-2.5 sm:px-4">
@@ -123,6 +133,24 @@ export function FilterPanel(props: Props): JSX.Element {
<div class="border-t border-line-soft">
<div class="grid grid-cols-1 items-start gap-x-8 gap-y-6 p-4 sm:p-5 md:grid-cols-2">
<div class="flex min-w-0 flex-col gap-5">
<div>
<label class={fieldLabel} for={daysId}>
Published in the last
</label>
<span class="inline-flex items-center gap-2">
<input
id={daysId}
type="number"
min="1"
step="1"
class={`${input} w-24 px-3 py-2.5 text-base`}
value={props.filters.releasedWithinDays}
onChange={(e) => commitDays(e.currentTarget)}
/>
days
</span>
</div>
<div>
<label class={fieldLabel} for={titleId}>
Title