this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add 'Recent searches'

+519 -157
+102
src/components/recent-searches.jsx
··· 1 + import { useAutoAnimate } from '@formkit/auto-animate/preact'; 2 + import { Trans, useLingui } from '@lingui/react/macro'; 3 + import { useReducer } from 'preact/hooks'; 4 + 5 + import { api } from '../utils/api'; 6 + import { 7 + addToSearchHistory, 8 + clearAllSearchHistory, 9 + getSearchHistory, 10 + removeFromSearchHistory, 11 + } from '../utils/search-history'; 12 + import showToast from '../utils/show-toast'; 13 + 14 + import Icon from './icon'; 15 + import Link from './link'; 16 + import { generateSearchItemData } from './search-form'; 17 + 18 + export default function RecentSearches({ onItemClick }) { 19 + const { t } = useLingui(); 20 + const { instance } = api(); 21 + const [, reload] = useReducer((c) => c + 1, 0); 22 + const history = getSearchHistory(); 23 + 24 + const handleClearAll = () => { 25 + clearAllSearchHistory(); 26 + showToast({ 27 + text: t`Cleared recent searches`, 28 + delay: 1000, 29 + }); 30 + reload(); 31 + }; 32 + 33 + const handleRemoveItem = (query, queryType) => { 34 + removeFromSearchHistory(query, queryType); 35 + reload(); 36 + }; 37 + 38 + const [listRef] = useAutoAnimate(); 39 + 40 + if (history.length === 0) { 41 + return null; 42 + } 43 + 44 + return ( 45 + <div class="recent-searches"> 46 + <div class="ui-state insignificant recent-searches-header"> 47 + <Icon icon="history" />{' '} 48 + <span> 49 + <Trans>Recent searches</Trans> 50 + </span> 51 + <span class="spacer" /> 52 + <button 53 + type="button" 54 + class="plain4 small" 55 + onClick={handleClearAll} 56 + disabled={history.length <= 0} 57 + > 58 + <span> 59 + <Trans>Clear all</Trans> 60 + </span> 61 + </button> 62 + </div> 63 + <ul class="link-list recent-searches-list" ref={listRef}> 64 + {history.map((historyItem) => { 65 + const { label, to, icon } = generateSearchItemData( 66 + historyItem.query, 67 + historyItem.queryType, 68 + instance, 69 + ); 70 + 71 + return ( 72 + <li 73 + key={`${historyItem.query}-${historyItem.queryType}-${historyItem.timestamp}`} 74 + class="recent-searches-item" 75 + > 76 + <Link 77 + to={to} 78 + class="recent-searches-link" 79 + onClick={(e) => { 80 + addToSearchHistory(historyItem.query, historyItem.queryType); 81 + onItemClick?.(e); 82 + }} 83 + > 84 + <Icon icon={icon} class="more-insignificant" /> 85 + <span class="recent-searches-label">{label}</span> 86 + </Link> 87 + <button 88 + type="button" 89 + class="plain4 small" 90 + onClick={() => 91 + handleRemoveItem(historyItem.query, historyItem.queryType) 92 + } 93 + > 94 + <Icon icon="trash" alt={t`Clear`} /> 95 + </button> 96 + </li> 97 + ); 98 + })} 99 + </ul> 100 + </div> 101 + ); 102 + }
+1 -1
src/components/search-command.css
··· 45 45 46 46 @media (min-width: 40em) { 47 47 #search-command-container { 48 - align-items: center; 48 + padding-top: max(calc(50vh - 10em), 16px); 49 49 background-image: radial-gradient( 50 50 closest-side, 51 51 var(--backdrop-color),
+4 -1
src/components/search-command.jsx
··· 57 57 }, 58 58 ); 59 59 60 + const hidden = !showSearch; 61 + 60 62 return ( 61 63 <div 62 64 id="search-command-container" 63 - hidden={!showSearch} 65 + hidden={hidden} 64 66 onClick={(e) => { 65 67 console.log(e); 66 68 if (e.target === e.currentTarget) { ··· 70 72 > 71 73 <SearchForm 72 74 ref={searchFormRef} 75 + hidden={hidden} 73 76 onSubmit={() => { 74 77 closeSearch(); 75 78 }}
+209 -108
src/components/search-form.jsx
··· 1 1 import { Trans, useLingui } from '@lingui/react/macro'; 2 2 import { forwardRef } from 'preact/compat'; 3 - import { useImperativeHandle, useRef, useState } from 'preact/hooks'; 3 + import { useImperativeHandle, useMemo, useRef, useState } from 'preact/hooks'; 4 4 import { useSearchParams } from 'react-router-dom'; 5 5 6 6 import { api } from '../utils/api'; 7 + import { addToSearchHistory, getSearchHistory } from '../utils/search-history'; 7 8 8 9 import Icon from './icon'; 9 10 import Link from './link'; 10 11 12 + // Helper function to generate search item data (label and URL) 13 + export const generateSearchItemData = (query, queryType, instance) => { 14 + let label, to, icon; 15 + 16 + if (queryType === 'statuses') { 17 + label = ( 18 + <Trans> 19 + Posts with <q>{query}</q> 20 + </Trans> 21 + ); 22 + to = `/search?q=${encodeURIComponent(query)}&type=statuses`; 23 + icon = 'document'; 24 + } else if (queryType === 'accounts') { 25 + label = ( 26 + <Trans> 27 + Accounts with <q>{query}</q> 28 + </Trans> 29 + ); 30 + to = `/search?q=${encodeURIComponent(query)}&type=accounts`; 31 + icon = 'group'; 32 + } else if (queryType === 'hashtags') { 33 + label = ( 34 + <Trans> 35 + Posts tagged with <mark>#{query.replace(/^#/, '')}</mark> 36 + </Trans> 37 + ); 38 + to = `/${instance}/t/${query.replace(/^#/, '')}`; 39 + icon = 'hashtag'; 40 + } else { 41 + // Default/general search 42 + label = ( 43 + <Trans> 44 + {query}{' '} 45 + <small class="insignificant">‒ accounts, hashtags &amp; posts</small> 46 + </Trans> 47 + ); 48 + to = `/search?q=${encodeURIComponent(query)}`; 49 + icon = 'search'; 50 + } 51 + 52 + return { label, to, icon }; 53 + }; 54 + 11 55 const SearchForm = forwardRef((props, ref) => { 12 56 const { t } = useLingui(); 13 57 const { instance } = api(); ··· 33 77 }, 34 78 })); 35 79 80 + const searchHistory = useMemo( 81 + () => getSearchHistory({ limit: 5 }), 82 + [props?.hidden], 83 + ); 84 + 85 + const searchSuggestionsData = useMemo(() => { 86 + if (!query) return []; 87 + 88 + const matchingHistory = searchHistory 89 + .filter((historyItem) => { 90 + // Filter out exact matches with current query 91 + if (historyItem.query === query) return false; 92 + // Check if history item contains the current query (case insensitive) 93 + return historyItem.query.toLowerCase().includes(query.toLowerCase()); 94 + }) 95 + .slice(0, 2); // Max 2 recent searches 96 + 97 + const recentSearchItems = matchingHistory.map((historyItem) => ({ 98 + ...generateSearchItemData( 99 + historyItem.query, 100 + historyItem.queryType, 101 + instance, 102 + ), 103 + queryType: historyItem.queryType, 104 + isRecentSearch: true, 105 + historyItem, 106 + })); 107 + 108 + const allItems = [ 109 + // General search 110 + { 111 + ...generateSearchItemData(query, null, instance), 112 + top: !type && !/\s/.test(query), 113 + hidden: !!type, 114 + }, 115 + // Recent searches 116 + ...recentSearchItems, 117 + // Posts search 118 + { 119 + ...generateSearchItemData(query, 'statuses', instance), 120 + hidden: /^https?:/.test(query), 121 + top: /\s/.test(query), 122 + queryType: 'statuses', 123 + }, 124 + // Hashtag search 125 + { 126 + ...generateSearchItemData(query, 'hashtags', instance), 127 + hidden: /^@/.test(query) || /^https?:/.test(query) || /\s/.test(query), 128 + top: /^#/.test(query), 129 + type: 'link', 130 + queryType: 'hashtags', 131 + }, 132 + // URL lookup (unique case) 133 + { 134 + label: ( 135 + <Trans> 136 + Look up <mark>{query}</mark> 137 + </Trans> 138 + ), 139 + to: `/${query}`, 140 + hidden: !/^https?:/.test(query), 141 + top: /^https?:/.test(query), 142 + type: 'link', 143 + icon: 'arrow-right', 144 + }, 145 + // Accounts search 146 + { 147 + ...generateSearchItemData(query, 'accounts', instance), 148 + queryType: 'accounts', 149 + }, 150 + ]; 151 + 152 + return allItems 153 + .sort((a, b) => { 154 + if (type) { 155 + if (a.queryType === type) return -1; 156 + if (b.queryType === type) return 1; 157 + } 158 + if (a.top && !b.top) return -1; 159 + if (!a.top && b.top) return 1; 160 + return 0; 161 + }) 162 + .filter(({ hidden }) => !hidden); 163 + }, [query, type, instance]); 164 + 36 165 return ( 37 166 <form 38 167 ref={formRef} ··· 60 189 location.hash = `/search`; 61 190 } 62 191 } 192 + 193 + addToSearchHistory(query, type); 63 194 64 195 props?.onSubmit?.(e); 65 196 }} ··· 87 218 }} 88 219 onFocus={() => { 89 220 setSearchMenuOpen(true); 90 - formRef.current 91 - ?.querySelector('.search-popover-item') 92 - ?.classList.add('focus'); 221 + // Focus first item 222 + const firstItem = formRef.current?.querySelector( 223 + '.search-popover-item', 224 + ); 225 + if (firstItem) { 226 + firstItem.classList.add('focus'); 227 + } 93 228 }} 94 229 onBlur={() => { 95 230 setTimeout(() => { ··· 158 293 }); 159 294 } 160 295 } else { 161 - const lastItem = document.querySelector( 162 - '.search-popover-item:last-child', 296 + const items = document.querySelectorAll( 297 + '.search-popover-item', 163 298 ); 299 + const lastItem = items[items.length - 1]; 164 300 if (lastItem) { 165 301 lastItem.classList.add('focus'); 166 302 } ··· 183 319 } 184 320 }} 185 321 /> 186 - <div class="search-popover" hidden={!searchMenuOpen || !query}> 187 - {/* {!!query && ( 188 - <Link 189 - to={`/search?q=${encodeURIComponent(query)}`} 190 - class="search-popover-item focus" 191 - onClick={(e) => { 192 - props?.onSubmit?.(e); 193 - }} 194 - > 195 - <Icon icon="search" /> 196 - <span>{query}</span> 197 - </Link> 198 - )} */} 199 - {!!query && 200 - [ 201 - { 202 - label: ( 203 - <Trans> 204 - {query}{' '} 205 - <small class="insignificant"> 206 - ‒ accounts, hashtags &amp; posts 207 - </small> 208 - </Trans> 209 - ), 210 - to: `/search?q=${encodeURIComponent(query)}`, 211 - top: !type && !/\s/.test(query), 212 - hidden: !!type, 213 - }, 214 - { 215 - label: ( 216 - <Trans> 217 - Posts with <q>{query}</q> 218 - </Trans> 219 - ), 220 - to: `/search?q=${encodeURIComponent(query)}&type=statuses`, 221 - hidden: /^https?:/.test(query), 222 - top: /\s/.test(query), 223 - icon: 'document', 224 - queryType: 'statuses', 225 - }, 226 - { 227 - label: ( 228 - <Trans> 229 - Posts tagged with <mark>#{query.replace(/^#/, '')}</mark> 230 - </Trans> 231 - ), 232 - to: `/${instance}/t/${query.replace(/^#/, '')}`, 233 - hidden: 234 - /^@/.test(query) || /^https?:/.test(query) || /\s/.test(query), 235 - top: /^#/.test(query), 236 - type: 'link', 237 - icon: 'hashtag', 238 - queryType: 'hashtags', 239 - }, 240 - { 241 - label: ( 242 - <Trans> 243 - Look up <mark>{query}</mark> 244 - </Trans> 245 - ), 246 - to: `/${query}`, 247 - hidden: !/^https?:/.test(query), 248 - top: /^https?:/.test(query), 249 - type: 'link', 250 - }, 251 - { 252 - label: ( 253 - <Trans> 254 - Accounts with <q>{query}</q> 255 - </Trans> 256 - ), 257 - to: `/search?q=${encodeURIComponent(query)}&type=accounts`, 258 - icon: 'group', 259 - queryType: 'accounts', 260 - }, 261 - ] 262 - .sort((a, b) => { 263 - if (type) { 264 - if (a.queryType === type) return -1; 265 - if (b.queryType === type) return 1; 322 + <div class="search-popover" hidden={!searchMenuOpen}> 323 + {/* Search History - show when no query */} 324 + {!query && searchHistory.length > 0 && ( 325 + <div class="search-popover-recent-searches"> 326 + <div class="search-popover-header"> 327 + <Icon icon="history" size="s" /> 328 + <Trans>Recent searches</Trans> 329 + </div> 330 + {searchHistory.map((historyItem, i) => { 331 + const { label, to, icon } = generateSearchItemData( 332 + historyItem.query, 333 + historyItem.queryType, 334 + instance, 335 + ); 336 + 337 + return ( 338 + <Link 339 + key={`${historyItem.query}-${historyItem.queryType}-${historyItem.timestamp}`} 340 + to={to} 341 + class={`search-popover-item ${i === 0 ? 'focus' : ''}`} 342 + onClick={(e) => { 343 + addToSearchHistory( 344 + historyItem.query, 345 + historyItem.queryType, 346 + ); 347 + props?.onSubmit?.(e); 348 + }} 349 + > 350 + <Icon icon={icon} class="more-insignificant" /> 351 + <span>{label}</span> 352 + </Link> 353 + ); 354 + })} 355 + <Link 356 + to="/search" 357 + class="search-popover-item search-history-see-all" 358 + > 359 + <Icon icon="more2" class="more-insignificant" /> 360 + <span> 361 + <Trans>See all</Trans> 362 + </span> 363 + </Link> 364 + </div> 365 + )} 366 + 367 + {/* Search Suggestions - show when there's a query */} 368 + {searchSuggestionsData.map( 369 + ({ label, to, icon, queryType, isRecentSearch, historyItem }, i) => ( 370 + <Link 371 + key={ 372 + isRecentSearch 373 + ? `recent-${historyItem.query}-${historyItem.queryType}-${historyItem.timestamp}` 374 + : `suggestion-${queryType || 'general'}-${i}` 266 375 } 267 - if (a.top && !b.top) return -1; 268 - if (!a.top && b.top) return 1; 269 - return 0; 270 - }) 271 - .filter(({ hidden }) => !hidden) 272 - .map(({ label, to, icon, type }, i) => ( 273 - <Link 274 - to={to} 275 - class={`search-popover-item ${i === 0 ? 'focus' : ''}`} 276 - // hidden={hidden} 277 - onClick={(e) => { 278 - console.log('onClick', e); 279 - props?.onSubmit?.(e); 280 - }} 281 - > 282 - <Icon 283 - icon={icon || (type === 'link' ? 'arrow-right' : 'search')} 284 - class="more-insignificant" 285 - /> 286 - <span>{label}</span>{' '} 287 - </Link> 288 - ))} 376 + to={to} 377 + class={`search-popover-item ${isRecentSearch ? 'search-popover-item-recent' : ''} ${i === 0 ? 'focus' : ''}`} 378 + onClick={(e) => { 379 + if (!isRecentSearch) { 380 + addToSearchHistory(query, queryType); 381 + } 382 + props?.onSubmit?.(e); 383 + }} 384 + > 385 + <Icon icon={icon} class="more-insignificant" /> 386 + <span>{label}</span> 387 + </Link> 388 + ), 389 + )} 289 390 </div> 290 391 </form> 291 392 );
+57 -42
src/locales/en.po
··· 101 101 #: src/components/account-info.jsx:454 102 102 #: src/components/account-info.jsx:856 103 103 #: src/pages/account-statuses.jsx:487 104 - #: src/pages/search.jsx:344 105 - #: src/pages/search.jsx:491 104 + #: src/pages/search.jsx:345 105 + #: src/pages/search.jsx:492 106 106 msgid "Posts" 107 107 msgstr "" 108 108 ··· 1077 1077 #: src/components/generic-accounts.jsx:154 1078 1078 #: src/components/notification.jsx:455 1079 1079 #: src/pages/accounts.jsx:50 1080 - #: src/pages/search.jsx:334 1081 - #: src/pages/search.jsx:367 1080 + #: src/pages/search.jsx:335 1081 + #: src/pages/search.jsx:368 1082 1082 msgid "Accounts" 1083 1083 msgstr "" 1084 1084 ··· 1086 1086 #: src/components/timeline.jsx:551 1087 1087 #: src/pages/list.jsx:321 1088 1088 #: src/pages/notifications.jsx:922 1089 - #: src/pages/search.jsx:561 1089 + #: src/pages/search.jsx:562 1090 1090 #: src/pages/status.jsx:1426 1091 1091 msgid "Show more…" 1092 1092 msgstr "" 1093 1093 1094 1094 #: src/components/generic-accounts.jsx:219 1095 1095 #: src/components/timeline.jsx:556 1096 - #: src/pages/search.jsx:566 1096 + #: src/pages/search.jsx:567 1097 1097 msgid "The end." 1098 1098 msgstr "" 1099 1099 ··· 1200 1200 1201 1201 #: src/components/keyboard-shortcuts-help.jsx:151 1202 1202 #: src/components/nav-menu.jsx:337 1203 - #: src/components/search-form.jsx:73 1203 + #: src/components/search-form.jsx:204 1204 1204 #: src/components/shortcuts-settings.jsx:52 1205 1205 #: src/components/shortcuts-settings.jsx:179 1206 - #: src/pages/search.jsx:46 1207 - #: src/pages/search.jsx:316 1206 + #: src/pages/search.jsx:47 1207 + #: src/pages/search.jsx:317 1208 1208 msgid "Search" 1209 1209 msgstr "" 1210 1210 ··· 1752 1752 msgid "Ending" 1753 1753 msgstr "" 1754 1754 1755 + #: src/components/recent-searches.jsx:27 1756 + msgid "Cleared recent searches" 1757 + msgstr "Cleared recent searches" 1758 + 1759 + #: src/components/recent-searches.jsx:49 1760 + #: src/components/search-form.jsx:328 1761 + msgid "Recent searches" 1762 + msgstr "Recent searches" 1763 + 1764 + #: src/components/recent-searches.jsx:59 1765 + msgid "Clear all" 1766 + msgstr "Clear all" 1767 + 1768 + #: src/components/recent-searches.jsx:94 1769 + #: src/pages/account-statuses.jsx:326 1770 + msgid "Clear" 1771 + msgstr "" 1772 + 1755 1773 #. Relative time in seconds, as short as possible 1756 1774 #. placeholder {0}: seconds < 1 ? 1 : Math.floor(seconds) 1757 1775 #: src/components/relative-time.jsx:61 ··· 1878 1896 msgid "Send Report <0>+ Block profile</0>" 1879 1897 msgstr "" 1880 1898 1881 - #: src/components/search-form.jsx:203 1882 - msgid "{query} <0>‒ accounts, hashtags & posts</0>" 1899 + #: src/components/search-form.jsx:18 1900 + msgid "Posts with <0>{query}</0>" 1883 1901 msgstr "" 1884 1902 1885 - #: src/components/search-form.jsx:216 1886 - msgid "Posts with <0>{query}</0>" 1903 + #: src/components/search-form.jsx:26 1904 + msgid "Accounts with <0>{query}</0>" 1887 1905 msgstr "" 1888 1906 1889 1907 #. placeholder {0}: query.replace(/^#/, '') 1890 - #: src/components/search-form.jsx:228 1908 + #: src/components/search-form.jsx:34 1891 1909 msgid "Posts tagged with <0>#{0}</0>" 1892 1910 msgstr "" 1893 1911 1894 - #: src/components/search-form.jsx:242 1912 + #: src/components/search-form.jsx:43 1913 + msgid "{query} <0>‒ accounts, hashtags & posts</0>" 1914 + msgstr "" 1915 + 1916 + #: src/components/search-form.jsx:135 1895 1917 msgid "Look up <0>{query}</0>" 1896 1918 msgstr "" 1897 1919 1898 - #: src/components/search-form.jsx:253 1899 - msgid "Accounts with <0>{query}</0>" 1920 + #: src/components/search-form.jsx:361 1921 + #: src/pages/home.jsx:251 1922 + msgid "See all" 1900 1923 msgstr "" 1901 1924 1902 1925 #: src/components/shortcuts-settings.jsx:48 ··· 2667 2690 msgid "Clear filters" 2668 2691 msgstr "" 2669 2692 2670 - #: src/pages/account-statuses.jsx:326 2671 - msgid "Clear" 2672 - msgstr "" 2673 - 2674 2693 #: src/pages/account-statuses.jsx:340 2675 2694 msgid "Showing post with replies" 2676 2695 msgstr "" ··· 2953 2972 2954 2973 #: src/pages/catchup.jsx:1319 2955 2974 #: src/pages/mentions.jsx:154 2956 - #: src/pages/search.jsx:329 2975 + #: src/pages/search.jsx:330 2957 2976 msgid "All" 2958 2977 msgstr "" 2959 2978 ··· 3309 3328 msgid "<0>New</0> <1>Follow Requests</1>" 3310 3329 msgstr "" 3311 3330 3312 - #: src/pages/home.jsx:251 3313 - msgid "See all" 3314 - msgstr "" 3315 - 3316 3331 #: src/pages/http-route.jsx:68 3317 3332 msgid "Resolving…" 3318 3333 msgstr "" ··· 3590 3605 msgid "Failed to delete scheduled post" 3591 3606 msgstr "Failed to delete scheduled post" 3592 3607 3593 - #: src/pages/search.jsx:50 3608 + #: src/pages/search.jsx:51 3594 3609 msgid "Search: {q} (Posts)" 3595 3610 msgstr "" 3596 3611 3597 - #: src/pages/search.jsx:53 3612 + #: src/pages/search.jsx:54 3598 3613 msgid "Search: {q} (Accounts)" 3599 3614 msgstr "" 3600 3615 3601 - #: src/pages/search.jsx:56 3616 + #: src/pages/search.jsx:57 3602 3617 msgid "Search: {q} (Hashtags)" 3603 3618 msgstr "" 3604 3619 3605 - #: src/pages/search.jsx:59 3620 + #: src/pages/search.jsx:60 3606 3621 msgid "Search: {q}" 3607 3622 msgstr "" 3608 3623 3609 - #: src/pages/search.jsx:339 3610 - #: src/pages/search.jsx:421 3624 + #: src/pages/search.jsx:340 3625 + #: src/pages/search.jsx:422 3611 3626 msgid "Hashtags" 3612 3627 msgstr "" 3613 3628 3614 - #: src/pages/search.jsx:371 3615 - #: src/pages/search.jsx:425 3616 - #: src/pages/search.jsx:495 3629 + #: src/pages/search.jsx:372 3630 + #: src/pages/search.jsx:426 3631 + #: src/pages/search.jsx:496 3617 3632 msgid "See more" 3618 3633 msgstr "" 3619 3634 3620 - #: src/pages/search.jsx:397 3635 + #: src/pages/search.jsx:398 3621 3636 msgid "See more accounts" 3622 3637 msgstr "" 3623 3638 3624 - #: src/pages/search.jsx:411 3639 + #: src/pages/search.jsx:412 3625 3640 msgid "No accounts found." 3626 3641 msgstr "" 3627 3642 3628 - #: src/pages/search.jsx:467 3643 + #: src/pages/search.jsx:468 3629 3644 msgid "See more hashtags" 3630 3645 msgstr "" 3631 3646 3632 - #: src/pages/search.jsx:481 3647 + #: src/pages/search.jsx:482 3633 3648 msgid "No hashtags found." 3634 3649 msgstr "" 3635 3650 3636 - #: src/pages/search.jsx:525 3651 + #: src/pages/search.jsx:526 3637 3652 msgid "See more posts" 3638 3653 msgstr "" 3639 3654 3640 - #: src/pages/search.jsx:539 3655 + #: src/pages/search.jsx:540 3641 3656 msgid "No posts found." 3642 3657 msgstr "" 3643 3658 3644 - #: src/pages/search.jsx:583 3659 + #: src/pages/search.jsx:585 3645 3660 msgid "Enter your search term or paste a URL above to get started." 3646 3661 msgstr "" 3647 3662
+94
src/pages/search.css
··· 122 122 display: flex; 123 123 gap: 8px; 124 124 align-items: center; 125 + border-radius: 0; 125 126 } 126 127 .search-popover-item[hidden] { 127 128 display: none; ··· 161 162 .search-popover-item:is(:hover, :focus, .focus) > .icon { 162 163 opacity: 1; 163 164 } 165 + 166 + /* Recent search items styling */ 167 + .search-popover-item-recent { 168 + background-color: var(--bg-faded-blur-color); 169 + } 170 + .search-popover-item-recent:is(:hover, :focus, .focus) { 171 + background-color: var(--link-bg-color); 172 + } 173 + 174 + .search-popover-header { 175 + background-image: linear-gradient( 176 + to bottom, 177 + var(--outline-color), 178 + transparent 179 + ); 180 + text-shadow: 0 1px var(--bg-color); 181 + padding: 8px 10px; 182 + color: var(--text-insignificant-color); 183 + font-size: 11px; 184 + font-weight: 500; 185 + text-transform: uppercase; 186 + pointer-events: none; 187 + user-select: none; 188 + display: flex; 189 + align-items: center; 190 + gap: 4px; 191 + } 192 + 193 + .search-history-see-all { 194 + font-size: smaller; 195 + color: var(--text-insignificant-color) !important; 196 + } 197 + .search-history-see-all:is(:hover, :focus, .focus) { 198 + color: var(--text-color) !important; 199 + background-color: var(--link-bg-color) !important; 200 + } 201 + 202 + /* Hide recent searches in popover when on search page */ 203 + #search-page .search-popover-recent-searches { 204 + display: none; 205 + } 206 + 207 + /* Recent Searches */ 208 + .recent-searches { 209 + .recent-searches-header { 210 + display: flex; 211 + align-items: center; 212 + gap: 8px; 213 + padding-inline-start: calc(16px + 12px); 214 + padding-block-end: 8px; 215 + text-align: start; 216 + } 217 + 218 + .recent-searches-list { 219 + padding-block: 0 !important; 220 + } 221 + 222 + .recent-searches-item { 223 + display: flex; 224 + background-color: var(--bg-faded-blur-color); 225 + align-items: center; 226 + 227 + @media (min-width: 40em) { 228 + background-color: var(--bg-blur-color); 229 + } 230 + 231 + --radius: 8px; 232 + &:first-child { 233 + border-start-start-radius: var(--radius); 234 + border-start-end-radius: var(--radius); 235 + } 236 + &:last-child { 237 + border-end-start-radius: var(--radius); 238 + border-end-end-radius: var(--radius); 239 + } 240 + 241 + button { 242 + margin: 0 8px; 243 + } 244 + } 245 + 246 + .recent-searches-link { 247 + flex-grow: 1; 248 + font-weight: normal; 249 + } 250 + 251 + .recent-searches-label :is(mark, q) { 252 + color: var(--text-color); 253 + background-color: var(--link-bg-color); 254 + unicode-bidi: isolate; 255 + direction: initial; 256 + } 257 + }
+9 -5
src/pages/search.jsx
··· 12 12 import Link from '../components/link'; 13 13 import Loader from '../components/loader'; 14 14 import NavMenu from '../components/nav-menu'; 15 + import RecentSearches from '../components/recent-searches'; 15 16 import SearchForm from '../components/search-form'; 16 17 import Status from '../components/status'; 17 18 import { api } from '../utils/api'; ··· 579 580 <Loader abrupt /> 580 581 </p> 581 582 ) : ( 582 - <p class="ui-state"> 583 - <Trans> 584 - Enter your search term or paste a URL above to get started. 585 - </Trans> 586 - </p> 583 + <> 584 + <p class="ui-state insignificant"> 585 + <Trans> 586 + Enter your search term or paste a URL above to get started. 587 + </Trans> 588 + </p> 589 + <RecentSearches /> 590 + </> 587 591 )} 588 592 </main> 589 593 </div>
+43
src/utils/search-history.js
··· 1 + import store from './store'; 2 + 3 + export const getSearchHistory = ({ limit } = {}) => { 4 + const history = store.account.get('searchHistory') || []; 5 + return limit ? history.slice(0, limit) : history; 6 + }; 7 + 8 + const MAX_HISTORY_LENGTH = 10; 9 + export const addToSearchHistory = (query, queryType = null) => { 10 + if (!query?.trim?.()) return; 11 + 12 + const history = getSearchHistory(); 13 + const existingIndex = history.findIndex( 14 + (item) => item.query === query && item.queryType === queryType, 15 + ); 16 + 17 + // LRU 18 + // Remove existing entry if found 19 + if (existingIndex !== -1) { 20 + history.splice(existingIndex, 1); 21 + } 22 + // Add to beginning (most recent) 23 + history.unshift({ 24 + query: query.trim(), 25 + queryType, 26 + timestamp: Date.now(), 27 + }); 28 + const limitedHistory = history.slice(0, MAX_HISTORY_LENGTH); 29 + 30 + store.account.set('searchHistory', limitedHistory); 31 + }; 32 + 33 + export const removeFromSearchHistory = (query, queryType = null) => { 34 + const history = getSearchHistory(); 35 + const filteredHistory = history.filter( 36 + (item) => !(item.query === query && item.queryType === queryType), 37 + ); 38 + store.account.set('searchHistory', filteredHistory); 39 + }; 40 + 41 + export const clearAllSearchHistory = () => { 42 + store.account.set('searchHistory', []); 43 + };