Skip to content

Commit a78d016

Browse files
CopilotStarefossen
andcommitted
fix: address Greptile review feedback and knip issues
- Fix tRPC usage: use api.useUtils().fetch() instead of useMutation() - Fix SponsorsSearchProvider: use SponsorExisting type and website field - Remove unused useProposalSearch hook and export - Update SearchModal stories to demo all 4 supported data types (Pages, Proposals, Speakers, Sponsors) - Fix React hooks exhaustive-deps warning Co-authored-by: Starefossen <968267+Starefossen@users.noreply.github.com>
1 parent 41b1b0b commit a78d016

9 files changed

Lines changed: 113 additions & 209 deletions

File tree

docs/ENHANCED_SEARCH_IMPLEMENTATION.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ Successfully implemented a unified search system for the admin interface that ex
1111
Created a provider-based architecture that makes it easy to add new search sources:
1212

1313
**Core Types** (`types.ts`):
14+
1415
- `SearchProvider` interface - defines contract for all search providers
15-
- `SearchCategory` enum - defines searchable categories
16+
- `SearchCategory` enum - defines searchable categories
1617
- `SearchResultItem` - standardized result format
1718
- `SearchResultGroup` - groups results by category
1819

@@ -21,32 +22,37 @@ Created a provider-based architecture that makes it easy to add new search sourc
2122
Implemented 4 search providers with different search strategies:
2223

2324
#### AdminPagesSearchProvider
25+
2426
- **Priority**: 1 (shown first)
2527
- **Searches**: Static list of admin pages
2628
- **Strategy**: Client-side keyword matching
2729
- **Results**: Quick navigation to admin pages (Dashboard, Proposals, Speakers, Sponsors, etc.)
2830

29-
#### ProposalsSearchProvider
31+
#### ProposalsSearchProvider
32+
3033
- **Priority**: 2
3134
- **Searches**: Proposal titles, descriptions, speakers, topics
3235
- **Strategy**: Server-side via existing `adminSearchProposals()` API
3336
- **Results**: Links to individual proposal detail pages
3437

3538
#### SponsorsSearchProvider
36-
- **Priority**: 3
39+
40+
- **Priority**: 3
3741
- **Searches**: Sponsor company names
3842
- **Strategy**: tRPC `sponsor.list({ query })` mutation
3943
- **Results**: Links to sponsors list page (where users can filter)
4044

4145
#### SpeakersSearchProvider
46+
4247
- **Priority**: 4
43-
- **Searches**: Speaker names, titles, emails, bios
48+
- **Searches**: Speaker names, titles, emails, bios
4449
- **Strategy**: tRPC `speakers.search({ query })` mutation
4550
- **Results**: Links to speakers list page (where users can filter)
4651

4752
### 3. Unified Search Hook (`src/lib/search/hooks/useUnifiedSearch.ts`)
4853

4954
Coordinates all search providers:
55+
5056
- Instantiates all providers with required dependencies (tRPC mutations)
5157
- Executes searches in parallel for fast results
5258
- Handles loading states and errors independently per provider
@@ -56,6 +62,7 @@ Coordinates all search providers:
5662
### 4. Updated SearchModal Component (`src/components/admin/SearchModal.tsx`)
5763

5864
Enhanced the existing modal:
65+
5966
- Replaced `useProposalSearch` with `useUnifiedSearch`
6067
- Displays results grouped by category with section headers
6168
- Shows appropriate icons for each result type (pages, proposals, speakers, sponsors)
@@ -73,17 +80,20 @@ Enhanced the existing modal:
7380
## Technical Implementation Details
7481

7582
### Performance Optimizations
83+
7684
- **300ms debounce**: Prevents excessive API calls while typing
77-
- **Parallel queries**: All providers search simultaneously
85+
- **Parallel queries**: All providers search simultaneously
7886
- **Error isolation**: Individual provider failures don't break the entire search
7987
- **Result prioritization**: Pages shown first, then proposals, speakers, sponsors
8088

8189
### Error Handling
90+
8291
- Each provider handles its own errors independently
8392
- Failed providers are logged but don't prevent other results from showing
8493
- User-friendly error messages in the UI
8594

8695
### Type Safety
96+
8797
- Full TypeScript coverage
8898
- Shared interfaces ensure consistent result format
8999
- tRPC integration provides end-to-end type safety for server calls
@@ -93,7 +103,7 @@ Enhanced the existing modal:
93103
The architecture makes adding new providers straightforward:
94104

95105
1. **Create provider class** implementing `SearchProvider` interface
96-
2. **Add category** to `SearchCategory` type
106+
2. **Add category** to `SearchCategory` type
97107
3. **Register provider** in `useUnifiedSearch` hook
98108
4. **Export provider** from providers index
99109

@@ -102,6 +112,7 @@ See `/src/lib/search/README.md` for detailed step-by-step instructions.
102112
## Future Enhancements
103113

104114
Ready to implement when needed (documented in README):
115+
105116
- **Orders search** - Search ticket purchases by order ID, attendee name, email, company
106117
- **Workshops search** - Search workshop registrations by attendee name, email
107118
- **Volunteers search** - Search volunteers by name, email
@@ -121,6 +132,7 @@ Ready to implement when needed (documented in README):
121132
## Files Changed
122133

123134
### Created
135+
124136
- `src/lib/search/types.ts` - Core types and interfaces
125137
- `src/lib/search/providers/AdminPagesSearchProvider.ts` - Static pages search
126138
- `src/lib/search/providers/ProposalsSearchProvider.ts` - Proposals search
@@ -132,6 +144,7 @@ Ready to implement when needed (documented in README):
132144
- `src/lib/search/README.md` - Comprehensive documentation
133145

134146
### Modified
147+
135148
- `src/components/admin/SearchModal.tsx` - Updated to use unified search
136149
- `src/components/admin/SearchModal.stories.tsx` - Updated with new examples
137150

src/components/admin/SearchModal.stories.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,26 @@ function MockSearchModal({
145145
</li>
146146
</ul>
147147
</li>
148+
<li>
149+
<h2 className="text-xs font-semibold text-gray-900 dark:text-white">
150+
Sponsors (1)
151+
</h2>
152+
<ul className="-mx-4 mt-2 text-sm text-gray-700 dark:text-gray-300">
153+
<li className="flex cursor-default items-center px-4 py-2 select-none">
154+
<div className="flex size-6 flex-none items-center justify-center rounded-full bg-gray-200 dark:bg-gray-700">
155+
<BuildingOfficeIcon className="size-4 text-gray-400 dark:text-gray-500" />
156+
</div>
157+
<div className="ml-3 flex-auto truncate">
158+
<div className="font-medium dark:text-white">
159+
Kubernetes Foundation
160+
</div>
161+
<div className="text-xs text-gray-500 dark:text-gray-400">
162+
kubernetes.io
163+
</div>
164+
</div>
165+
</li>
166+
</ul>
167+
</li>
148168
</ul>
149169
)}
150170

@@ -213,7 +233,7 @@ export const WithResults: Story = {
213233
docs: {
214234
description: {
215235
story:
216-
'Search results grouped into multiple categories (Pages, Proposals, Speakers), with icons for each type.',
236+
'Search results grouped into all 4 supported categories (Pages, Proposals, Speakers, Sponsors), with icons for each type.',
217237
},
218238
},
219239
},

src/components/admin/SearchModal.tsx

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -118,57 +118,55 @@ export function SearchModal({ open, onClose }: SearchModalProps) {
118118
/>
119119
</div>
120120

121-
{!isSearching &&
122-
query &&
123-
searchResults.totalCount > 0 && (
124-
<ComboboxOptions
125-
static
126-
as="ul"
127-
className="max-h-80 transform-gpu scroll-py-10 scroll-pb-2 space-y-4 overflow-y-auto p-4 pb-2"
128-
>
129-
{searchResults.groups.map((group) => (
130-
<li key={group.category}>
131-
<h2 className="text-xs font-semibold text-gray-900 dark:text-white">
132-
{group.label} ({group.items.length})
133-
</h2>
134-
<ul className="-mx-4 mt-2 text-sm text-gray-700 dark:text-gray-300">
135-
{group.items.map((item) => {
136-
const Icon = item.icon || DocumentTextIcon
137-
return (
138-
<ComboboxOption
139-
as="li"
140-
key={item.id}
141-
value={item}
142-
className="group flex cursor-default items-center px-4 py-2 select-none data-focus:bg-indigo-600 data-focus:text-white data-focus:outline-hidden dark:data-focus:bg-indigo-500"
143-
>
144-
<div className="shrink-0">
145-
<div className="flex size-6 flex-none items-center justify-center rounded-full bg-gray-200 group-data-focus:bg-white/20 dark:bg-gray-700 dark:group-data-focus:bg-white/20">
146-
<Icon className="size-4 text-gray-400 group-data-focus:text-white dark:text-gray-500" />
147-
</div>
121+
{!isSearching && query && searchResults.totalCount > 0 && (
122+
<ComboboxOptions
123+
static
124+
as="ul"
125+
className="max-h-80 transform-gpu scroll-py-10 scroll-pb-2 space-y-4 overflow-y-auto p-4 pb-2"
126+
>
127+
{searchResults.groups.map((group) => (
128+
<li key={group.category}>
129+
<h2 className="text-xs font-semibold text-gray-900 dark:text-white">
130+
{group.label} ({group.items.length})
131+
</h2>
132+
<ul className="-mx-4 mt-2 text-sm text-gray-700 dark:text-gray-300">
133+
{group.items.map((item) => {
134+
const Icon = item.icon || DocumentTextIcon
135+
return (
136+
<ComboboxOption
137+
as="li"
138+
key={item.id}
139+
value={item}
140+
className="group flex cursor-default items-center px-4 py-2 select-none data-focus:bg-indigo-600 data-focus:text-white data-focus:outline-hidden dark:data-focus:bg-indigo-500"
141+
>
142+
<div className="shrink-0">
143+
<div className="flex size-6 flex-none items-center justify-center rounded-full bg-gray-200 group-data-focus:bg-white/20 dark:bg-gray-700 dark:group-data-focus:bg-white/20">
144+
<Icon className="size-4 text-gray-400 group-data-focus:text-white dark:text-gray-500" />
148145
</div>
149-
<div className="ml-3 flex-auto truncate">
150-
<div className="font-medium dark:text-white">
151-
{item.title}
152-
</div>
153-
{item.subtitle && (
154-
<div className="text-xs text-gray-500 group-data-focus:text-white/70 dark:text-gray-400">
155-
{item.subtitle}
156-
</div>
157-
)}
158-
{item.description && (
159-
<div className="text-xs text-gray-500 group-data-focus:text-white/70 dark:text-gray-400">
160-
{item.description}
161-
</div>
162-
)}
146+
</div>
147+
<div className="ml-3 flex-auto truncate">
148+
<div className="font-medium dark:text-white">
149+
{item.title}
163150
</div>
164-
</ComboboxOption>
165-
)
166-
})}
167-
</ul>
168-
</li>
169-
))}
170-
</ComboboxOptions>
171-
)}
151+
{item.subtitle && (
152+
<div className="text-xs text-gray-500 group-data-focus:text-white/70 dark:text-gray-400">
153+
{item.subtitle}
154+
</div>
155+
)}
156+
{item.description && (
157+
<div className="text-xs text-gray-500 group-data-focus:text-white/70 dark:text-gray-400">
158+
{item.description}
159+
</div>
160+
)}
161+
</div>
162+
</ComboboxOption>
163+
)
164+
})}
165+
</ul>
166+
</li>
167+
))}
168+
</ComboboxOptions>
169+
)}
172170

173171
{isSearching && (
174172
<div className="max-h-80 transform-gpu scroll-py-10 scroll-pb-2 space-y-4 overflow-y-auto p-4 pb-2">

src/components/admin/hooks/useProposalSearch.ts

Lines changed: 0 additions & 130 deletions
This file was deleted.

src/components/admin/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export * from './LoadingSkeleton'
6161
export * from './PageLoadingSkeleton'
6262

6363
export { useFilterStateWithURL, useProposalFiltering } from './hooks'
64-
export { useProposalSearch } from './hooks/useProposalSearch'
6564

6665
export type { FilterState } from './ProposalsFilter'
6766
export type { AdminPageHeaderProps, StatCardProps } from './AdminPageHeader'

0 commit comments

Comments
 (0)