Skip to content

Commit

Permalink
Add select to sort search results
Browse files Browse the repository at this point in the history
  • Loading branch information
tillprochaska committed Jan 23, 2025
1 parent 78df8de commit b385d32
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 6 deletions.
3 changes: 2 additions & 1 deletion frontend/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"Select",
"GroupsFilterSelect",
"CountriesFilterSelect",
"PositionFilterSelect"
"PositionFilterSelect",
"SortSelect"
]
}
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/client.entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Eyes from "./components/Eyes";
import GroupStatsList from "./components/GroupStatsList";
import MemberVotesList from "./components/MemberVotesList";
import ShareButton from "./components/ShareButton";
import SortSelect from "./components/SortSelect";
import VoteTabs from "./components/VoteTabs";
import { hydrateIslands } from "./lib/islands";

Expand All @@ -15,4 +16,5 @@ hydrateIslands([
Eyes,
ShareButton,
Banner,
SortSelect,
]);
30 changes: 30 additions & 0 deletions frontend/src/components/SortSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Select from "./Select";

type SortSelectProps = {
value?: string;
};

export default function SortSelect({ value }: SortSelectProps) {
return (
<Select
value={value}
options={{
relevance: "Relevance",
newest: "Newest first",
oldest: "Oldest first",
}}
onChange={(event) => {
const searchParams = new URLSearchParams(window.location.search);
const value = event.currentTarget.value;

if (value) {
searchParams.set("sort", value);
} else {
searchParams.delete("sort");
}

window.location.search = searchParams.toString();
}}
/>
);
}
18 changes: 18 additions & 0 deletions frontend/src/lib/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { oneOf } from "./validation";

describe("oneOf", () => {
it("returns value if it is allowed", () => {
assert.strictEqual(oneOf("foo", ["foo", "bar"]), "foo");
assert.strictEqual(oneOf("foo", ["foo", "bar"], "bar"), "foo");
});

it("returns fallback if value is not allowed", () => {
assert.strictEqual(oneOf("foo", ["bar", "baz"], "bar"), "bar");
});

it("returns null if value is not allowed and no fallback is defined", () => {
assert.strictEqual(oneOf("foo", ["bar", "baz"]), null);
});
});
21 changes: 21 additions & 0 deletions frontend/src/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function oneOf<T extends readonly string[], F extends string>(
value: string,
allowed: T,
fallback: F,
): T[number] | F;
export function oneOf<T extends readonly string[]>(
value: string,
allowed: T,
fallback: never,
): T[number] | null;
export function oneOf<T extends readonly string[]>(
value: string,
allowed: T,
fallback?: string,
) {
if (!allowed.includes(value)) {
return fallback || null;
}

return value;
}
12 changes: 12 additions & 0 deletions frontend/src/pages/SearchPage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.search-page__info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--text-sm);
color: var(--color-text-light);
}

.search-page__sort {
display: flex;
align-items: center;
}
48 changes: 43 additions & 5 deletions frontend/src/pages/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,51 @@ import BaseLayout from "../components/BaseLayout";
import Hero from "../components/Hero";
import Pagination from "../components/Pagination";
import SearchForm from "../components/SearchForm";
import SortSelect from "../components/SortSelect";
import Stack from "../components/Stack";
import VoteCards from "../components/VoteCards";
import Wrapper from "../components/Wrapper";
import { firstQueryValue, redirect } from "../lib/http";
import { Island } from "../lib/islands";
import { getLogger } from "../lib/logging";
import type { Loader, Page, Request } from "../lib/server";
import { oneOf } from "../lib/validation";

import "./SearchPage.css";

const log = getLogger();

export const loader: Loader<VotesQueryResponse> = async (request: Request) => {
const SORT_PARAMS = {
relevance: {},
newest: { sort_by: "timestamp", sort_order: "desc" },
oldest: { sort_by: "timestamp", sort_order: "asc" },
} as const;

type SearchPageData = VotesQueryResponse & {
sort: "relevance" | "newest" | "oldest";
};

export const loader: Loader<SearchPageData> = async (request: Request) => {
const q = firstQueryValue(request.query, "q") || "";

const page = Number.parseInt(
firstQueryValue(request.query, "page") || "1",
10,
);

const { data } = await searchVotes({ query: { q, page } });
const sort = oneOf(
firstQueryValue(request.query, "sort") || "",
["relevance", "newest", "oldest"] as const,
"relevance",
);

const { data } = await searchVotes({
query: {
q,
page,
...SORT_PARAMS[sort],
},
});

if (!request.isBot && q) {
log.info({
Expand All @@ -37,7 +65,7 @@ export const loader: Loader<VotesQueryResponse> = async (request: Request) => {
redirect(`/votes/${data.results[0].id}`);
}

return data;
return { ...data, sort };
};

function pageUrl(query: string, page: number): string {
Expand All @@ -54,7 +82,7 @@ function pageUrl(query: string, page: number): string {
return `/votes?${params.toString()}`;
}

export const SearchPage: Page<VotesQueryResponse> = ({ data, request }) => {
export const SearchPage: Page<SearchPageData> = ({ data, request }) => {
const query = firstQueryValue(request.query, "q") || "";

return (
Expand All @@ -67,8 +95,18 @@ export const SearchPage: Page<VotesQueryResponse> = ({ data, request }) => {
action={<SearchForm style="elevated" value={query} />}
/>
<div class="px">
<Wrapper>
<Wrapper className="search-page">
<Stack space="lg">
<div class="search-page__info">
<div>{data.total} results</div>
<label class="search-page__sort">
Sort by:
<Island>
<SortSelect value={data.sort} />
</Island>
</label>
</div>

<VoteCards
groupByDate={!request.query.q}
votes={data.results}
Expand Down

0 comments on commit b385d32

Please sign in to comment.