From 632ba89301038df4f336b7a19c3dd5d206f50035 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 28 Aug 2026 22:34:05 +0200 Subject: [PATCH] chore: improve suggestions --- src/components/NavSearch.css | 8 - src/components/NavSearch.tsx | 382 ++++++++--------------------------- 2 files changed, 82 insertions(+), 308 deletions(-) diff --git a/src/components/NavSearch.css b/src/components/NavSearch.css index f9d88f4..49c33f7 100644 --- a/src/components/NavSearch.css +++ b/src/components/NavSearch.css @@ -458,14 +458,6 @@ font-weight: 700; } -/* ── Matching Text Highlight ── */ -.nav-search-highlight { - background: transparent; - color: var(--primary, #f97316); - font-weight: 700; - text-decoration: underline; - text-underline-offset: 2px; -} /* ── States ── */ .nav-search-loading-state, diff --git a/src/components/NavSearch.tsx b/src/components/NavSearch.tsx index d819279..ae3ef54 100644 --- a/src/components/NavSearch.tsx +++ b/src/components/NavSearch.tsx @@ -5,9 +5,6 @@ import { Search, X, Clock, - User, - Folder, - Tag, Package, ArrowRight, CornerDownLeft, @@ -33,22 +30,11 @@ export interface SearchSuggestionPlugin { is_preorder: boolean; } -export interface SearchSuggestionAuthor { - id: number; - username: string; - plugin_count: number; -} - -export interface SearchSuggestionCategory { - name: string; - plugin_count: number; -} - export interface SearchSuggestionsResponse { plugins: SearchSuggestionPlugin[]; - authors: SearchSuggestionAuthor[]; - categories: SearchSuggestionCategory[]; - tags: string[]; + authors?: any[]; + categories?: any[]; + tags?: string[]; } const RECENT_SEARCHES_KEY = 'pumpkin_market_recent_searches'; @@ -113,42 +99,9 @@ const setCachedSuggestions = (term: string, data: SearchSuggestionsResponse) => clientSuggestionCache.set(term.toLowerCase(), data); }; -const HighlightMatch: React.FC<{ text: string; query: string }> = ({ text, query }) => { - if (!query.trim()) return <>{text}; - const trimmed = query.trim(); - const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const parts = text.split(new RegExp(`(${escaped})`, 'gi')); - return ( - <> - {parts.map((part, i) => - part.toLowerCase() === trimmed.toLowerCase() ? ( - - {part} - - ) : ( - part - ) - )} - - ); -}; - -const getAuthorAccentColor = (name: string) => { - const colors = [ - '#f97316', '#3b82f6', '#10b981', '#8b5cf6', - '#ec4899', '#06b6d4', '#eab308', '#14b8a6', - ]; - let hash = 0; - for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); - return colors[Math.abs(hash) % colors.length]; -}; - type FlattenedItem = | { kind: 'recent'; query: string } | { kind: 'plugin'; plugin: SearchSuggestionPlugin } - | { kind: 'author'; author: SearchSuggestionAuthor } - | { kind: 'category'; category: SearchSuggestionCategory } - | { kind: 'tag'; tag: string } | { kind: 'view_all'; query: string }; interface NavSearchProps { @@ -290,18 +243,6 @@ export const NavSearch: React.FC = ({ suggestions.plugins.forEach((plugin) => { items.push({ kind: 'plugin', plugin }); }); - // Categories - suggestions.categories.forEach((category) => { - items.push({ kind: 'category', category }); - }); - // Authors - suggestions.authors.forEach((author) => { - items.push({ kind: 'author', author }); - }); - // Tags - suggestions.tags.forEach((tag) => { - items.push({ kind: 'tag', tag }); - }); // View all results items.push({ kind: 'view_all', query: trimmed }); } @@ -345,36 +286,6 @@ export const NavSearch: React.FC = ({ onNavigate?.(); break; } - case 'author': { - saveRecentSearch(item.author.username); - refreshRecentSearches(); - navigate(`/profile/${encodeURIComponent(item.author.username)}`); - setIsOpen(false); - setSelectedIndex(-1); - inputRef.current?.blur(); - onNavigate?.(); - break; - } - case 'category': { - saveRecentSearch(item.category.name); - refreshRecentSearches(); - navigate(`/search?category=${encodeURIComponent(item.category.name)}`); - setIsOpen(false); - setSelectedIndex(-1); - inputRef.current?.blur(); - onNavigate?.(); - break; - } - case 'tag': { - saveRecentSearch(item.tag); - refreshRecentSearches(); - navigate(`/search?q=${encodeURIComponent(item.tag)}`); - setIsOpen(false); - setSelectedIndex(-1); - inputRef.current?.blur(); - onNavigate?.(); - break; - } case 'view_all': { executeSearch(item.query); break; @@ -435,12 +346,7 @@ export const NavSearch: React.FC = ({ refreshRecentSearches(); }; - const hasSuggestions = - suggestions && - (suggestions.plugins.length > 0 || - suggestions.authors.length > 0 || - suggestions.categories.length > 0 || - suggestions.tags.length > 0); + const hasSuggestions = Boolean(suggestions && suggestions.plugins.length > 0); const showRecentDropdown = isOpen && !query.trim() && recentSearches.length > 0; const showSuggestionsDropdown = isOpen && query.trim().length > 0; @@ -563,221 +469,97 @@ export const NavSearch: React.FC = ({ )} - {suggestions && ( + {suggestions && hasSuggestions && (
- {/* Category matches */} - {suggestions.categories.length > 0 && ( -
-
- - - {t('search.categories_title')} - -
- {suggestions.categories.map((cat) => { - const itemIndex = flattenedItems.findIndex( - (it) => it.kind === 'category' && it.category.name === cat.name - ); - const isSelected = selectedIndex === itemIndex; - return ( -
handleSelectItem({ kind: 'category', category: cat })} - role="option" - aria-selected={isSelected} - > -
- -
- - - - - {cat.plugin_count === 1 - ? t('search.plugin_count') - : t('search.plugins_count', { count: cat.plugin_count })} - -
- ); - })} -
- )} - {/* Plugin matches */} - {suggestions.plugins.length > 0 && ( -
-
- - - {t('search.plugins_title')} - -
- {suggestions.plugins.map((plugin) => { - const itemIndex = flattenedItems.findIndex( - (it) => it.kind === 'plugin' && it.plugin.id === plugin.id - ); - const isSelected = selectedIndex === itemIndex; +
+
+ + + {t('search.plugins_title')} + +
+ {suggestions.plugins.map((plugin) => { + const itemIndex = flattenedItems.findIndex( + (it) => it.kind === 'plugin' && it.plugin.id === plugin.id + ); + const isSelected = selectedIndex === itemIndex; - const isFree = plugin.type === 'free'; - const isSale = plugin.sale_active && plugin.sale_discount_percent > 0; - const price = plugin.price_cents / 100; - const discountedPrice = isSale - ? (price * (1 - plugin.sale_discount_percent / 100)).toFixed(2) - : price.toFixed(2); + const isFree = plugin.type === 'free'; + const isSale = plugin.sale_active && plugin.sale_discount_percent > 0; + const price = plugin.price_cents / 100; + const discountedPrice = isSale + ? (price * (1 - plugin.sale_discount_percent / 100)).toFixed(2) + : price.toFixed(2); - return ( -
handleSelectItem({ kind: 'plugin', plugin })} - role="option" - aria-selected={isSelected} - > -
- {plugin.preview_path ? ( - {plugin.name} - ) : ( -
- {plugin.name.charAt(0).toUpperCase()} -
+ return ( +
handleSelectItem({ kind: 'plugin', plugin })} + role="option" + aria-selected={isSelected} + > +
+ {plugin.preview_path ? ( + {plugin.name} + ) : ( +
+ {plugin.name.charAt(0).toUpperCase()} +
+ )} +
+ +
+
+ + {plugin.name} + + {plugin.is_preorder && ( + + {t('plugin.pre_order')} + )}
- -
-
- - - - {plugin.is_preorder && ( - - {t('plugin.pre_order')} - - )} -
-
- - by - - {plugin.category && ( - <> - · - {plugin.category} - - )} -
+
+ + by {plugin.dev_name} + + {plugin.category && ( + <> + · + {plugin.category} + + )}
+
-
- {isFree ? ( - - {t('plugin.free')} +
+ {isFree ? ( + + {t('plugin.free')} + + ) : isSale ? ( +
+ + -{plugin.sale_discount_percent}% - ) : isSale ? ( -
- - -{plugin.sale_discount_percent}% - - - €{discountedPrice} - -
- ) : ( - €{price.toFixed(2)} + €{discountedPrice} - )} -
-
- ); - })} -
- )} - - {/* Author matches */} - {suggestions.authors.length > 0 && ( -
-
- - - {t('search.authors_title')} - -
- {suggestions.authors.map((author) => { - const itemIndex = flattenedItems.findIndex( - (it) => it.kind === 'author' && it.author.id === author.id - ); - const isSelected = selectedIndex === itemIndex; - const accentColor = getAuthorAccentColor(author.username); - - return ( -
handleSelectItem({ kind: 'author', author })} - role="option" - aria-selected={isSelected} - > -
- {author.username.charAt(0).toUpperCase()} -
-
- - +
+ ) : ( + + €{price.toFixed(2)} -
- - {author.plugin_count === 1 - ? t('search.plugin_count') - : t('search.plugins_count', { count: author.plugin_count })} - + )}
- ); - })} -
- )} - - {/* Tags matches */} - {suggestions.tags.length > 0 && ( -
-
- - - {t('search.tags_title')} - -
-
- {suggestions.tags.map((tag) => { - const itemIndex = flattenedItems.findIndex( - (it) => it.kind === 'tag' && it.tag === tag - ); - const isSelected = selectedIndex === itemIndex; - return ( - - ); - })} -
-
- )} +
+ ); + })} +
)}