Skip to content

Commit 16237ad

Browse files
committed
feat(web): add result sorting to search page
Users can now sort search results by relevance, citations, works count, name (alphabetical), or entity type. The sort is performed client-side on the autocomplete results. Changes: - Add Select import from @mantine/core - Add SortOption type with 5 sort options - Add SORT_OPTIONS constant for select data - Add sortBy state with "relevance" as default - Add sortedResults memo that sorts based on selected option - Add sort selector dropdown next to view mode toggle - Replace displayResults with sortedResults throughout render
1 parent 9924fb3 commit 16237ad

File tree

1 file changed

+73
-32
lines changed

1 file changed

+73
-32
lines changed

apps/web/src/routes/search.lazy.tsx

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Container,
1313
Group,
1414
Paper,
15+
Select,
1516
SegmentedControl,
1617
SimpleGrid,
1718
Stack,
@@ -47,6 +48,16 @@ interface SearchFilters {
4748

4849
type ViewMode = "table" | "card" | "list";
4950

51+
type SortOption = "relevance" | "citations" | "works" | "name" | "type";
52+
53+
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
54+
{ value: "relevance", label: "Relevance" },
55+
{ value: "citations", label: "Most Cited" },
56+
{ value: "works", label: "Most Works" },
57+
{ value: "name", label: "Name (A-Z)" },
58+
{ value: "type", label: "Entity Type" },
59+
];
60+
5061
// Calculate entity type breakdown from results
5162
const getEntityTypeBreakdown = (results: AutocompleteResult[]) => {
5263
const breakdown = results.reduce((acc, result) => {
@@ -190,6 +201,9 @@ const SearchPage = () => {
190201
// View mode state
191202
const [viewMode, setViewMode] = useState<ViewMode>("table");
192203

204+
// Sort state
205+
const [sortBy, setSortBy] = useState<SortOption>("relevance");
206+
193207
// Entity type filter state
194208
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
195209

@@ -409,6 +423,25 @@ const SearchPage = () => {
409423
return searchResults.filter(result => selectedTypes.includes(result.entity_type));
410424
}, [searchResults, selectedTypes]);
411425

426+
// Sort and filter results by selected sort option
427+
const sortedResults = useMemo(() => {
428+
let results = filteredResults;
429+
430+
switch (sortBy) {
431+
case "citations":
432+
return [...results].sort((a, b) => (b.cited_by_count || 0) - (a.cited_by_count || 0));
433+
case "works":
434+
return [...results].sort((a, b) => (b.works_count || 0) - (a.works_count || 0));
435+
case "name":
436+
return [...results].sort((a, b) => a.display_name.localeCompare(b.display_name));
437+
case "type":
438+
return [...results].sort((a, b) => a.entity_type.localeCompare(b.entity_type));
439+
case "relevance":
440+
default:
441+
return results; // Keep original order from API (relevance-sorted)
442+
}
443+
}, [filteredResults, sortBy]);
444+
412445
// Calculate entity type breakdown
413446
const entityTypeBreakdown = useMemo(() => {
414447
return searchResults ? getEntityTypeBreakdown(searchResults) : [];
@@ -433,8 +466,6 @@ const SearchPage = () => {
433466
);
434467
if (!hasResults) return renderNoResultsState(searchFilters.query, handleQuickSearch);
435468

436-
const displayResults = selectedTypes.length > 0 ? filteredResults : searchResults;
437-
438469
return (
439470
<Stack>
440471
{/* Enhanced Results Header */}
@@ -443,7 +474,7 @@ const SearchPage = () => {
443474
<Group justify="space-between" align="center" wrap="nowrap">
444475
<Group gap="md" align="center">
445476
<Text size="sm" fw={500}>
446-
{displayResults.length} {displayResults.length === 1 ? 'result' : 'results'}
477+
{sortedResults.length} {sortedResults.length === 1 ? 'result' : 'results'}
447478
{selectedTypes.length > 0 && ` (filtered from ${searchResults.length})`}
448479
</Text>
449480
{searchDuration > 0 && (
@@ -455,32 +486,42 @@ const SearchPage = () => {
455486
)}
456487
</Group>
457488

458-
{/* View mode toggle */}
459-
<SegmentedControl
460-
value={viewMode}
461-
onChange={(value) => setViewMode(value as ViewMode)}
462-
data={[
463-
{
464-
value: 'table',
465-
label: (
466-
<Tooltip label="Table view"><IconTable size={ICON_SIZE.SM} /></Tooltip>
467-
)
468-
},
469-
{
470-
value: 'card',
471-
label: (
472-
<Tooltip label="Card view"><IconLayoutGrid size={ICON_SIZE.SM} /></Tooltip>
473-
)
474-
},
475-
{
476-
value: 'list',
477-
label: (
478-
<Tooltip label="List view"><IconList size={ICON_SIZE.SM} /></Tooltip>
479-
)
480-
},
481-
]}
482-
size="xs"
483-
/>
489+
{/* View mode toggle and sort selector */}
490+
<Group gap="sm">
491+
<Select
492+
size="xs"
493+
value={sortBy}
494+
onChange={(value) => setSortBy(value as SortOption)}
495+
data={SORT_OPTIONS}
496+
style={{ width: 140 }}
497+
allowDeselect={false}
498+
/>
499+
<SegmentedControl
500+
value={viewMode}
501+
onChange={(value) => setViewMode(value as ViewMode)}
502+
data={[
503+
{
504+
value: 'table',
505+
label: (
506+
<Tooltip label="Table view"><IconTable size={ICON_SIZE.SM} /></Tooltip>
507+
)
508+
},
509+
{
510+
value: 'card',
511+
label: (
512+
<Tooltip label="Card view"><IconLayoutGrid size={ICON_SIZE.SM} /></Tooltip>
513+
)
514+
},
515+
{
516+
value: 'list',
517+
label: (
518+
<Tooltip label="List view"><IconList size={ICON_SIZE.SM} /></Tooltip>
519+
)
520+
},
521+
]}
522+
size="xs"
523+
/>
524+
</Group>
484525
</Group>
485526

486527
{/* Entity type breakdown and filter chips */}
@@ -600,7 +641,7 @@ const SearchPage = () => {
600641
</Table.Tr>
601642
</Table.Thead>
602643
<Table.Tbody>
603-
{displayResults.map((result) => {
644+
{sortedResults.map((result) => {
604645
const entityUrl = convertToRelativeUrl(result.id);
605646
const inGraph = isInGraph(result.id);
606647
return (
@@ -664,7 +705,7 @@ const SearchPage = () => {
664705

665706
{viewMode === "card" && (
666707
<SimpleGrid cols={{ base: 1, xs: 2, sm: 2, md: 3, lg: 4 }} spacing="md">
667-
{displayResults.map((result) => {
708+
{sortedResults.map((result) => {
668709
const entityUrl = convertToRelativeUrl(result.id);
669710
const inGraph = isInGraph(result.id);
670711
return (
@@ -717,7 +758,7 @@ const SearchPage = () => {
717758

718759
{viewMode === "list" && (
719760
<Stack gap="xs">
720-
{displayResults.map((result) => {
761+
{sortedResults.map((result) => {
721762
const entityUrl = convertToRelativeUrl(result.id);
722763
const inGraph = isInGraph(result.id);
723764
return (

0 commit comments

Comments
 (0)