Monorepo for Tangled tangled.org
856
fork

Configure Feed

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

appview: add topbar quick-search #331

open opened by eti.tf targeting master from eti.tf/core-2: eti/search-in-nav

Adds an inline search widget to the topbar.

On desktop, a search input appears directly in the nav with a dropdown of quick results (keyboard-navigable with ↑↓/Enter/Escape, ⌘K to focus). On mobile, a search button opens a fullscreen overlay with the same results.

Signed-off-by: eti eti@eti.tf

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xu5apv6kmu5jp7g5hwdnej42/sh.tangled.repo.pull/3ml6n7w7fmq22
+421 -18
Diff #0
+1
.gitignore
··· 6 6 appview/pages/static/* 7 7 result 8 8 !.gitkeep 9 + !appview/pages/static/topbar-search.js 9 10 out/ 10 11 node_modules/ 11 12 patches
+19
appview/pages/pages.go
··· 1674 1674 return p.execute("search/search", w, params) 1675 1675 } 1676 1676 1677 + type SearchQuickParams struct { 1678 + Repos []models.Repo 1679 + Query string 1680 + Total int 1681 + } 1682 + 1683 + func (p *Pages) SearchQuick(w io.Writer, params SearchQuickParams) error { 1684 + return p.executePlain("search/fragments/quick", w, params) 1685 + } 1686 + 1687 + func (p *Pages) SearchQuickMobile(w io.Writer, params SearchQuickParams) error { 1688 + tpl, err := p.parse("search/fragments/quick") 1689 + if err != nil { 1690 + return err 1691 + } 1692 + return tpl.ExecuteTemplate(w, "search/fragments/quickMobile", params) 1693 + } 1694 + 1695 + 1677 1696 func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1678 1697 return p.execute("timeline/home", w, params) 1679 1698 }
+171
appview/pages/static/topbar-search.js
··· 1 + (function () { 2 + if (window._navSearchReady) return; 3 + window._navSearchReady = true; 4 + 5 + var scrollY = 0; 6 + var vvListener = null; 7 + var mobileResultsTouchLock = null; 8 + 9 + function updateOverlayHeight() { 10 + var overlay = document.getElementById('mobile-search-overlay'); 11 + if (!overlay) return; 12 + var vv = window.visualViewport; 13 + var layoutH = window.innerHeight; 14 + var visH = vv ? vv.height : layoutH; 15 + overlay.style.height = layoutH + 'px'; 16 + overlay.style.top = '0px'; 17 + var spacer = document.getElementById('mobile-search-spacer'); 18 + if (spacer) spacer.style.height = Math.max(0, layoutH - visH) + 'px'; 19 + } 20 + 21 + function openMobile() { 22 + var overlay = document.getElementById('mobile-search-overlay'); 23 + if (!overlay) return; 24 + if (overlay.classList.contains('opacity-100')) return; 25 + overlay.classList.remove('opacity-0', 'pointer-events-none'); 26 + overlay.classList.add('opacity-100', 'pointer-events-auto'); 27 + overlay.setAttribute('aria-hidden', 'false'); 28 + scrollY = window.scrollY; 29 + document.body.style.position = 'fixed'; 30 + document.body.style.top = '-' + scrollY + 'px'; 31 + document.body.style.width = '100%'; 32 + updateOverlayHeight(); 33 + if (window.visualViewport) { 34 + vvListener = updateOverlayHeight; 35 + window.visualViewport.addEventListener('resize', vvListener); 36 + } 37 + var input = document.getElementById('mobile-search-input'); 38 + if (input) input.focus({ preventScroll: true }); 39 + var results = document.getElementById('mobile-search-results'); 40 + if (results && !mobileResultsTouchLock) { 41 + mobileResultsTouchLock = function (e) { e.preventDefault(); }; 42 + results.addEventListener('touchmove', mobileResultsTouchLock, { passive: false }); 43 + } 44 + } 45 + 46 + function closeMobile() { 47 + var overlay = document.getElementById('mobile-search-overlay'); 48 + if (!overlay) return; 49 + overlay.classList.remove('opacity-100', 'pointer-events-auto'); 50 + overlay.classList.add('opacity-0', 'pointer-events-none'); 51 + overlay.setAttribute('aria-hidden', 'true'); 52 + overlay.style.height = ''; 53 + overlay.style.top = ''; 54 + var spacer = document.getElementById('mobile-search-spacer'); 55 + if (spacer) { spacer.style.height = ''; spacer.classList.add('hidden'); } 56 + if (window.visualViewport && vvListener) { 57 + window.visualViewport.removeEventListener('resize', vvListener); 58 + vvListener = null; 59 + } 60 + document.body.style.position = ''; 61 + document.body.style.top = ''; 62 + document.body.style.width = ''; 63 + window.scrollTo(0, scrollY); 64 + var input = document.getElementById('mobile-search-input'); 65 + if (input) { input.value = ''; input.blur(); } 66 + var results = document.getElementById('mobile-search-results'); 67 + if (results) results.innerHTML = ''; 68 + } 69 + 70 + function clearDesktop() { 71 + var r = document.getElementById('topbar-search-results'); 72 + if (r) r.innerHTML = ''; 73 + var b = document.getElementById('topbar-search-box'); 74 + if (b) { b.classList.add('rounded'); b.classList.remove('rounded-t'); } 75 + } 76 + 77 + function submitFromInput(input) { 78 + var v = input.value.trim(); 79 + if (v) window.location.href = '/search?q=' + encodeURIComponent(v); 80 + } 81 + 82 + document.addEventListener('click', function (e) { 83 + var t = e.target.closest('[data-action]'); 84 + if (t) { 85 + var a = t.getAttribute('data-action'); 86 + if (a === 'open-mobile-search') { e.preventDefault(); openMobile(); return; } 87 + if (a === 'close-mobile-search') { e.preventDefault(); closeMobile(); return; } 88 + } 89 + var c = document.getElementById('topbar-search-container'); 90 + if (c && !c.contains(e.target)) clearDesktop(); 91 + }); 92 + 93 + // Defer so a click on a result fires before results are cleared. 94 + document.addEventListener('focusout', function (e) { 95 + var c = document.getElementById('topbar-search-container'); 96 + if (c && c.contains(e.target) && !c.contains(e.relatedTarget)) { 97 + setTimeout(clearDesktop, 0); 98 + } 99 + }); 100 + 101 + document.addEventListener('htmx:afterSwap', function (e) { 102 + var t = e.detail.target; 103 + if (!t) return; 104 + if (t.id === 'topbar-search-results') { 105 + var b = document.getElementById('topbar-search-box'); 106 + var open = t.children.length > 0; 107 + if (b) { b.classList.toggle('rounded', !open); b.classList.toggle('rounded-t', open); } 108 + return; 109 + } 110 + if (t.id === 'mobile-search-results') { 111 + if (mobileResultsTouchLock) { 112 + t.removeEventListener('touchmove', mobileResultsTouchLock); 113 + mobileResultsTouchLock = null; 114 + } 115 + var spacer = document.getElementById('mobile-search-spacer'); 116 + if (!spacer) return; 117 + var hasResults = !!t.querySelector('[data-results-footer]'); 118 + spacer.classList.toggle('hidden', !hasResults); 119 + } 120 + }); 121 + 122 + document.addEventListener('keydown', function (e) { 123 + var input = document.getElementById('topbar-search-input'); 124 + var results = document.getElementById('topbar-search-results'); 125 + var mobileOverlay = document.getElementById('mobile-search-overlay'); 126 + var mobileInput = document.getElementById('mobile-search-input'); 127 + var active = document.activeElement; 128 + 129 + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { 130 + e.preventDefault(); 131 + if (input) { input.focus(); input.select(); } 132 + return; 133 + } 134 + 135 + if (e.key === 'Enter') { 136 + if (active === input) { e.preventDefault(); submitFromInput(input); return; } 137 + if (active === mobileInput) { e.preventDefault(); submitFromInput(mobileInput); return; } 138 + } 139 + 140 + if (e.key === 'Escape') { 141 + if (mobileOverlay && !mobileOverlay.classList.contains('opacity-0')) { 142 + e.preventDefault(); 143 + closeMobile(); 144 + return; 145 + } 146 + if (input) { 147 + var ls = results ? Array.from(results.querySelectorAll('[data-nav-result]')) : []; 148 + if (active === input || ls.indexOf(active) >= 0) { 149 + e.preventDefault(); 150 + clearDesktop(); 151 + input.blur(); 152 + } 153 + } 154 + } 155 + 156 + if (!input || !results) return; 157 + var links = Array.from(results.querySelectorAll('[data-nav-result]')); 158 + var inInput = active === input; 159 + var idx = links.indexOf(active); 160 + 161 + if (e.key === 'ArrowDown') { 162 + if (inInput && links.length) { e.preventDefault(); links[0].focus(); } 163 + else if (idx >= 0 && idx < links.length - 1) { e.preventDefault(); links[idx + 1].focus(); } 164 + } 165 + 166 + if (e.key === 'ArrowUp') { 167 + if (idx === 0) { e.preventDefault(); input.focus(); } 168 + else if (idx > 0) { e.preventDefault(); links[idx - 1].focus(); } 169 + } 170 + }); 171 + })();
+4 -3
appview/pages/templates/layouts/base.html
··· 3 3 <html lang="en" class="dark:bg-gray-900"> 4 4 <head> 5 5 <meta charset="UTF-8" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> 7 7 <meta name="description" content="The next-generation social coding platform."/> 8 8 <meta name="htmx-config" content='{"includeIndicatorStyles": false}'> 9 9 ··· 23 23 <script defer src="/static/htmx.min.js"></script> 24 24 <script defer src="/static/htmx-ext-ws.min.js"></script> 25 25 <script defer src="/static/actor-typeahead.js" type="module"></script> 26 + <script defer src="/static/topbar-search.js"></script> 26 27 27 28 <link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/> 28 29 <link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/> ··· 68 69 </head> 69 70 <body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200 {{ block "bodyClasses" . }} {{ end }}"> 70 71 {{ block "topbarLayout" . }} 71 - <header class="w-full col-span-full md:col-span-1 md:col-start-2 drop-shadow-sm dark:text-white bg-white dark:bg-gray-800" style="z-index: 20;"> 72 + <header class="w-full col-span-full md:col-span-1 md:col-start-2 shadow-sm dark:text-white bg-white dark:bg-gray-800 pt-[env(safe-area-inset-top)]" style="z-index: 20;"> 72 73 73 74 {{ if .LoggedInUser }} 74 75 <div id="upgrade-banner" ··· 100 101 {{ end }} 101 102 102 103 {{ block "footerLayout" . }} 103 - <footer class="mt-12"> 104 + <footer class="mt-12 pb-[env(safe-area-inset-bottom)]"> 104 105 {{ template "layouts/fragments/footer" . }} 105 106 </footer> 106 107 {{ end }}
+67 -5
appview/pages/templates/layouts/fragments/topbar.html
··· 9 9 10 10 <div id="right-items" class="flex items-center gap-4"> 11 11 {{ with .LoggedInUser }} 12 - {{ block "newButton" . }} {{ end }} 13 - {{ template "notifications/fragments/bell" }} 14 - {{ block "searchButton" . }} {{ end }} 15 - {{ block "profileDropdown" . }} {{ end }} 12 + <div class="flex items-center order-1 md:order-3">{{ block "newButton" . }} {{ end }}</div> 13 + <div class="flex items-center order-2 md:order-4">{{ template "notifications/fragments/bell" }}</div> 14 + <div class="flex items-center order-3 md:order-1">{{ block "searchButton" . }} {{ end }}</div> 15 + <div class="hidden md:block md:order-2 w-px h-6 bg-gray-200 dark:bg-gray-700 self-center"></div> 16 + <div class="flex items-center order-4 md:order-5">{{ block "profileDropdown" . }} {{ end }}</div> 16 17 {{ else }} 17 18 <a href="/login">login</a> 18 19 <span class="text-gray-500 dark:text-gray-400">or</span> ··· 44 45 {{ end }} 45 46 46 47 {{ define "searchButton" }} 47 - <a href="/search">{{ i "search" "size-5 text-gray-500 dark:text-gray-400" }}</a> 48 + <div class="relative hidden md:block" id="topbar-search-container"> 49 + <div id="topbar-search-box" class="relative flex items-center gap-2 px-2 pr-1.5 py-4 max-h-[30px] w-80 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-900 focus-within:z-[51] before:content-[''] before:absolute before:inset-0 before:rounded-md before:invisible focus-within:before:visible before:ring-2 before:ring-transparent before:ring-offset-transparent before:ring-offset-1 before:pointer-events-none focus-within:before:ring-gray-300 dark:focus-within:before:ring-gray-600"> 50 + {{ i "search" "size-4 text-gray-400" }} 51 + <input 52 + type="text" 53 + id="topbar-search-input" 54 + name="q" 55 + placeholder="search..." 56 + autocomplete="off" 57 + class="flex-1 border-none bg-transparent p-0 focus:outline-none focus:[box-shadow:none] text-sm text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-600 peer" 58 + hx-get="/search/quick" 59 + hx-trigger="input changed delay:250ms" 60 + hx-target="#topbar-search-results" 61 + hx-swap="innerHTML" 62 + hx-indicator="#topbar-search-indicator" 63 + /> 64 + <kbd class="pointer-events-none flex items-center shrink-0 text-xs border border-gray-200 dark:border-gray-600 rounded border-b-2 px-1 font-sans leading-5 text-gray-400 dark:text-gray-500 peer-focus:hidden peer-[:not(:placeholder-shown)]:hidden">⌘K</kbd> 65 + <span id="topbar-search-indicator" class="hidden [&.htmx-request]:block shrink-0"> 66 + {{ i "loader-circle" "size-4 text-gray-400 animate-spin" }} 67 + </span> 68 + </div> 69 + <div id="topbar-search-results" class="absolute right-0 top-full z-50 w-80"></div> 70 + </div> 71 + 72 + <button id="mobile-search-btn" class="md:hidden" type="button" data-action="open-mobile-search"> 73 + {{ i "search" "size-5 text-gray-500 dark:text-gray-400" }} 74 + </button> 75 + 76 + <div id="mobile-search-overlay" 77 + class="md:hidden fixed inset-x-0 top-0 h-dvh z-50 78 + bg-white dark:bg-gray-900 79 + flex flex-col 80 + opacity-0 pointer-events-none 81 + transition-opacity duration-150 82 + pt-[env(safe-area-inset-top)]" 83 + aria-hidden="true"> 84 + <div class="flex items-center gap-3 px-4 py-3 border-b border-gray-200 dark:border-gray-700 shrink-0"> 85 + {{ i "search" "size-4 text-gray-400 shrink-0" }} 86 + <input 87 + type="text" 88 + id="mobile-search-input" 89 + name="q" 90 + placeholder="search..." 91 + autocomplete="off" 92 + inputmode="search" 93 + class="flex-1 min-w-0 border-none bg-transparent p-0 focus:outline-none focus:[box-shadow:none] text-[16px] text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-600" 94 + hx-get="/search/quick/mobile" 95 + hx-trigger="input changed delay:250ms" 96 + hx-target="#mobile-search-results" 97 + hx-swap="innerHTML" 98 + hx-indicator="#mobile-search-indicator" 99 + /> 100 + <span id="mobile-search-indicator" class="hidden [&.htmx-request]:block shrink-0"> 101 + {{ i "loader-circle" "size-4 text-gray-400 animate-spin" }} 102 + </span> 103 + <button type="button" class="shrink-0 p-1 -mr-1 hit-area hit-area-4" data-action="close-mobile-search"> 104 + {{ i "x" "size-5 text-gray-400" }} 105 + </button> 106 + </div> 107 + <div id="mobile-search-results" class="flex-1 flex flex-col overflow-hidden pb-[env(safe-area-inset-bottom)]"></div> 108 + <div id="mobile-search-spacer" class="shrink-0 bg-gray-50 dark:bg-gray-800 hidden"></div> 109 + </div> 48 110 {{ end }} 49 111 50 112 {{ define "profileDropdown" }}
+78
appview/pages/templates/search/fragments/quick.html
··· 1 + {{define "search/fragments/quick"}} 2 + {{- if .Repos -}} 3 + <div class="bg-white dark:bg-gray-900 border border-t-0 border-gray-200 dark:border-gray-700 rounded-b-md shadow-xl overflow-hidden"> 4 + <div class="max-h-[70vh] overflow-y-auto overscroll-contain divide-y divide-gray-100 dark:divide-gray-700"> 5 + {{ template "search/fragments/quickPage" . }} 6 + </div> 7 + {{ template "search/fragments/quickFooter" . }} 8 + </div> 9 + {{- else if .Query -}} 10 + <div class="bg-white dark:bg-gray-900 border border-t-0 border-gray-200 dark:border-gray-700 rounded-b-md shadow-xl overflow-hidden px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"> 11 + no results for "{{ .Query }}" 12 + </div> 13 + {{- end -}} 14 + {{end}} 15 + 16 + {{define "search/fragments/quickMobile"}} 17 + {{- if .Repos -}} 18 + <div class="flex flex-col flex-1 min-h-0 overscroll-contain"> 19 + <div class="flex-1 overflow-y-auto overscroll-contain [&>*+*]:border-t [&>*+*]:border-gray-200 dark:[&>*+*]:border-gray-700"> 20 + {{ template "search/fragments/quickPage" . }} 21 + </div> 22 + {{ template "search/fragments/quickFooter" . }} 23 + </div> 24 + {{- else if .Query -}} 25 + <div class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"> 26 + no results for "{{ .Query }}" 27 + </div> 28 + {{- end -}} 29 + {{end}} 30 + 31 + {{define "search/fragments/quickPage"}} 32 + {{- range .Repos -}} 33 + {{ template "search/fragments/quickItem" . }} 34 + {{- end -}} 35 + {{end}} 36 + 37 + {{define "search/fragments/quickItem"}} 38 + {{ $owner := resolve .Did }} 39 + <a href="/{{ $owner }}/{{ .Name }}" 40 + data-nav-result 41 + class="flex flex-col gap-1.5 px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/50 focus:bg-gray-100 dark:focus:bg-gray-700/50 focus:outline-none no-underline hover:no-underline last:border-b last:border-gray-200 dark:last:border-gray-700"> 42 + <div class="flex items-center gap-2 font-medium text-gray-800 dark:text-gray-200"> 43 + {{ i "book-marked" "size-4 pt-0.5 text-gray-400" }} 44 + <span class="truncate">{{ $owner }}/{{ .Name }}</span> 45 + </div> 46 + {{- with .Description -}} 47 + <div class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 pl-6">{{ . }}</div> 48 + {{- end -}} 49 + {{- with .RepoStats -}} 50 + {{- if or .Language .StarCount .IssueCount.Open .PullCount.Open -}} 51 + <div class="flex gap-3 items-center text-xs text-gray-400 pl-6 font-mono"> 52 + {{- with .Language -}} 53 + <span class="flex items-center gap-1">{{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }}{{ . }}</span> 54 + {{- end -}} 55 + {{- with .StarCount -}} 56 + <span class="flex items-center gap-1">{{ i "star" "w-3 h-3" }} {{ . }}</span> 57 + {{- end -}} 58 + {{- with .IssueCount.Open -}} 59 + <span class="flex items-center gap-1">{{ i "circle-dot" "w-3 h-3" }} {{ . }}</span> 60 + {{- end -}} 61 + {{- with .PullCount.Open -}} 62 + <span class="flex items-center gap-1">{{ i "git-pull-request" "w-3 h-3" }} {{ . }}</span> 63 + {{- end -}} 64 + </div> 65 + {{- end -}} 66 + {{- end -}} 67 + </a> 68 + {{end}} 69 + 70 + {{define "search/fragments/quickFooter"}} 71 + <div data-results-footer class="bg-gray-50 dark:bg-gray-800 px-4 py-2 flex justify-between items-center text-gray-600 dark:text-gray-400 border-t border-gray-100 dark:border-gray-700"> 72 + <span>{{ .Total }} results</span> 73 + <a href="/search?q={{ urlquery .Query }}" 74 + class="flex items-center gap-1 hover:text-gray-600 dark:hover:text-gray-300 no-underline hover:no-underline"> 75 + all results {{ i "arrow-right" "size-4" }} 76 + </a> 77 + </div> 78 + {{end}}
+2
appview/state/router.go
··· 161 161 r.Post("/logout", s.Logout) 162 162 163 163 r.With(middleware.Paginate).Get("/search", s.Search) 164 + r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick", s.SearchQuick) 165 + r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick/mobile", s.SearchQuickMobile) 164 166 165 167 r.Post("/account/switch", s.SwitchAccount) 166 168 r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount)
+79 -10
appview/state/search.go
··· 1 1 package state 2 2 3 3 import ( 4 + "cmp" 4 5 "net/http" 6 + "slices" 5 7 "strings" 6 8 "time" 7 9 ··· 68 70 return 69 71 } 70 72 71 - // sort repos to match search result order (by relevance) 72 - repoMap := make(map[int64]models.Repo, len(repos)) 73 - for _, repo := range repos { 74 - repoMap[repo.Id] = repo 75 - } 76 - repos = make([]models.Repo, 0, len(res.Hits)) 77 - for _, id := range res.Hits { 78 - if repo, ok := repoMap[id]; ok { 79 - repos = append(repos, repo) 80 - } 73 + hitIdx := make(map[int64]int, len(res.Hits)) 74 + for i, id := range res.Hits { 75 + hitIdx[id] = i 81 76 } 77 + slices.SortFunc(repos, func(a, b models.Repo) int { 78 + return cmp.Compare(hitIdx[a.Id], hitIdx[b.Id]) 79 + }) 82 80 } 83 81 resultCount = int(res.Total) 84 82 ··· 158 156 } 159 157 } 160 158 159 + func (s *State) SearchQuick(w http.ResponseWriter, r *http.Request) { 160 + s.searchQuick(w, r, false) 161 + } 162 + 163 + func (s *State) SearchQuickMobile(w http.ResponseWriter, r *http.Request) { 164 + s.searchQuick(w, r, true) 165 + } 166 + 167 + func (s *State) searchQuick(w http.ResponseWriter, r *http.Request, mobile bool) { 168 + rawQuery := r.URL.Query().Get("q") 169 + if rawQuery == "" { 170 + w.WriteHeader(http.StatusOK) 171 + return 172 + } 173 + 174 + const pageSize = 5 175 + 176 + query := searchquery.Parse(rawQuery) 177 + tf := searchquery.ExtractTextFilters(query) 178 + 179 + searchOpts := models.RepoSearchOptions{ 180 + Keywords: tf.Keywords, 181 + Phrases: tf.Phrases, 182 + NegatedKeywords: tf.NegatedKeywords, 183 + NegatedPhrases: tf.NegatedPhrases, 184 + Page: pagination.Page{Limit: pageSize}, 185 + } 186 + 187 + var repos []models.Repo 188 + var total int 189 + 190 + if searchOpts.HasSearchFilters() { 191 + res, err := s.indexer.Repos.Search(r.Context(), searchOpts) 192 + if err != nil { 193 + s.logger.Error("failed quick search", "err", err) 194 + http.Error(w, "search failed", http.StatusInternalServerError) 195 + return 196 + } 197 + total = int(res.Total) 198 + if len(res.Hits) > 0 { 199 + repos, err = db.GetRepos(s.db, orm.FilterIn("id", res.Hits)) 200 + if err != nil { 201 + s.logger.Error("failed to get repos for quick search", "err", err) 202 + http.Error(w, "search failed", http.StatusInternalServerError) 203 + return 204 + } 205 + hitIdx := make(map[int64]int, len(res.Hits)) 206 + for i, id := range res.Hits { 207 + hitIdx[id] = i 208 + } 209 + slices.SortFunc(repos, func(a, b models.Repo) int { 210 + return cmp.Compare(hitIdx[a.Id], hitIdx[b.Id]) 211 + }) 212 + } 213 + } 214 + 215 + params := pages.SearchQuickParams{ 216 + Repos: repos, 217 + Query: rawQuery, 218 + Total: total, 219 + } 220 + 221 + render := s.pages.SearchQuick 222 + if mobile { 223 + render = s.pages.SearchQuickMobile 224 + } 225 + if err := render(w, params); err != nil { 226 + s.logger.Error("failed to render quick search", "err", err) 227 + } 228 + } 229 + 161 230 // parseSortParam parses sort parameter like "stars-desc" or "created-asc" 162 231 func parseSortParam(sortParam string) (string, bool) { 163 232 defaultSort := func() (string, bool) { return "relevance", true }

History

3 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview: add topbar quick-search
merge conflicts detected
expand
  • .gitignore:6
  • appview/pages/pages.go:1674
  • appview/pages/templates/layouts/base.html:3
  • appview/pages/templates/layouts/fragments/topbar.html:9
  • appview/pages/templates/user/fragments/repoCard.html:5
  • appview/state/router.go:165
  • appview/state/search.go:1
expand 0 comments
5 commits
expand
appview: add topbar quick-search
appview: improve topbar quick-search ux
appview: modernize topbar-search.js
appview: use shared repoCard in quick-search results
appview: make quick-search more responsive
expand 0 comments
eti.tf submitted #0
1 commit
expand
appview: add topbar quick-search
expand 0 comments