Expand 20 hidden lines 21 21 use crate :: error:: { AppError , AppResult } ; 22 22 use crate :: icons:: { Icon , icon} ; 23 23 use crate :: layout:: { Chrome , page} ; 24 - use crate :: repo:: { RepoEntry , repo_card} ; 24 + use crate :: repo:: { self , RepoEntry , repo_card} ; 25 25 use crate :: session:: { 26 26 MaybeUser , RequireUser , THEME_COOKIE , clear_cookie, ensure_csrf, session_cookie, verify_csrf, 27 27 } ; Expand 43 hidden lines 71 71 let body = dashboard_body ( & state, & u) . await ?; 72 72 Ok ( ( jar, page ( & chrome, "Dashboard" , body) ) . into_response ( ) ) 73 73 } else { 74 - let body = explore_body ( & state) . await ?; 74 + let body = explore_repos_body ( & state, "" ) . await ?; 75 75 Ok ( ( jar, page ( & chrome, "Explore" , body) ) . into_response ( ) ) 76 76 } 77 77 } Expand 4 hidden lines 82 82 MaybeUser ( user) : MaybeUser , 83 83 jar : CookieJar , 84 84 uri : Uri , 85 + axum:: extract:: Query ( lq) : axum:: extract:: Query < repo:: ListQuery > , 85 86 ) -> AppResult < Response > { 86 87 let ( jar, chrome) = build_chrome ( & state, jar, user, uri. path ( ) . to_string ( ) ) ; 87 - let body = explore_body ( & state) . await ?; 88 + let body = explore_repos_body ( & state, & lq. q. unwrap_or_default ( ) ) . await ?; 88 89 Ok ( ( jar, page ( & chrome, "Explore" , body) ) . into_response ( ) ) 89 90 } 90 91 91 - 92 - async fn dashboard_body ( state : & AppState , user : & User ) -> AppResult < Markup > { 93 - let mine = state. store. repos_by_owner ( & user. id) . await ?; 94 - let entries: Vec < RepoEntry > = mine 95 - . iter ( ) 96 - . map ( |r| RepoEntry :: owned ( & user. username, r) ) 97 - . collect ( ) ; 98 - Ok ( html ! { 99 - h1 { "Welcome, " ( user. display_name. clone ( ) . unwrap_or_else ( || user. username. clone ( ) ) ) } 100 - ( repo_card ( "Your repositories" , "You have no repositories yet." , & entries) ) 101 - } ) 92 + 93 + pub async fn explore_users ( 94 + State ( state) : State < AppState > , 95 + MaybeUser ( user) : MaybeUser , 96 + jar : CookieJar , 97 + uri : Uri , 98 + axum:: extract:: Query ( lq) : axum:: extract:: Query < repo:: ListQuery > , 99 + ) -> AppResult < Response > { 100 + let ( jar, chrome) = build_chrome ( & state, jar, user, uri. path ( ) . to_string ( ) ) ; 101 + let body = explore_users_body ( & state, & lq. q. unwrap_or_default ( ) ) . await ?; 102 + Ok ( ( jar, page ( & chrome, "Explore" , body) ) . into_response ( ) ) 103 + } 104 + 105 + 106 + fn explore_subnav ( active : & str ) -> Markup { 107 + html ! { 108 + nav class="subnav" aria-label="Explore" { 109 + a href="/explore" aria-current=[ ( active == "repos" ) . then_some ( "page" ) ] { 110 + ( icon ( Icon :: Box ) ) span { "Repositories" } 111 + } 112 + a href="/explore/users" aria-current=[ ( active == "users" ) . then_some ( "page" ) ] { 113 + ( icon ( Icon :: User ) ) span { "Users" } 114 + } 115 + } 116 + } 102 117 } 103 118 104 - 105 - async fn explore_body ( state : & AppState ) -> AppResult < Markup > { 119 + 120 + async fn explore_repos_body ( state : & AppState , q : & str ) -> AppResult < Markup > { 121 + let needle = q. trim ( ) . to_lowercase ( ) ; 106 122 let all = state. store. list_repos ( ) . await ?; 107 123 let public: Vec < _ > = all. into_iter ( ) . filter ( |r| !r. is_private) . collect ( ) ; 108 - 124 + 109 125 let mut owners: HashMap < String , String > = HashMap :: new ( ) ; 110 126 for repo in & public { 111 127 if !owners. contains_key ( & repo. owner_id) { Expand 7 hidden lines 119 135 } 120 136 let entries: Vec < RepoEntry > = public 121 137 . iter ( ) 122 - . map ( |r| { 138 + . filter_map ( |r| { 123 139 let owner = owners. get ( & r. owner_id) . map_or ( "" , String :: as_str) ; 124 - RepoEntry :: global ( owner, r) 140 + let label = format ! ( "{owner}/{}" , r. path) ; 141 + ( needle. is_empty ( ) || label. to_lowercase ( ) . contains ( & needle) ) 142 + . then ( || RepoEntry :: global ( owner, r) ) 125 143 } ) 126 144 . collect ( ) ; 127 145 Ok ( html ! { 128 - h1 { "Explore" } 146 + ( explore_subnav ( "repos" ) ) 147 + form class="search-row" method="get" action="/explore" { 148 + input type ="search" name="q" value=( q) placeholder="Search repositories…" 149 + aria-label="Search repositories" ; 150 + button class="btn btn-primary" type ="submit" { "Search" } 151 + } 129 152 ( repo_card ( "Public repositories" , "No public repositories." , & entries) ) 130 153 } ) 131 154 } 132 155 156 + 157 + async fn explore_users_body ( state : & AppState , q : & str ) -> AppResult < Markup > { 158 + let needle = q. trim ( ) . to_lowercase ( ) ; 159 + let users: Vec < User > = state 160 + . store 161 + . list_users ( ) 162 + . await ? 163 + . into_iter ( ) 164 + . filter ( |u| u. disabled_at. is_none ( ) ) 165 + . filter ( |u| { 166 + needle. is_empty ( ) 167 + || u. username. to_lowercase ( ) . contains ( & needle) 168 + || u. display_name 169 + . as_deref ( ) 170 + . is_some_and ( |d| d. to_lowercase ( ) . contains ( & needle) ) 171 + } ) 172 + . collect ( ) ; 173 + Ok ( html ! { 174 + ( explore_subnav ( "users" ) ) 175 + form class="search-row" method="get" action="/explore/users" { 176 + input type ="search" name="q" value=( q) placeholder="Search users…" 177 + aria-label="Search users" ; 178 + button class="btn btn-primary" type ="submit" { "Search" } 179 + } 180 + ( user_card ( & users) ) 181 + } ) 182 + } 183 + 184 + 185 + fn user_card ( users : & [ User ] ) -> Markup { 186 + html ! { 187 + section class="listing" { 188 + h2 { "Users" } 189 + @if users. is_empty ( ) { 190 + div class="card" { p class="muted" { "No users." } } 191 + } @else { 192 + ul class="repo-list card" { 193 + @for u in users { 194 + li { 195 + a class="user-row" href={ "/" ( u. username) } { 196 + img class="avatar" src={ "/avatar/" ( u. username) } alt="" ; 197 + span class="user-row-main" { 198 + span class="user-row-name" { 199 + ( u. display_name. as_deref ( ) . unwrap_or ( & u. username) ) 200 + } 201 + span class="user-row-meta muted" { 202 + "@" ( u. username) " · Joined on " ( repo:: fmt_joined ( u. created_at) ) 203 + } 204 + } 205 + } 206 + } 207 + } 208 + } 209 + } 210 + } 211 + } 212 + } 213 + 214 + 215 + async fn dashboard_body ( state : & AppState , user : & User ) -> AppResult < Markup > { 216 + let mine = state. store. repos_by_owner ( & user. id) . await ?; 217 + let entries: Vec < RepoEntry > = mine 218 + . iter ( ) 219 + . map ( |r| RepoEntry :: owned ( & user. username, r) ) 220 + . collect ( ) ; 221 + Ok ( html ! { 222 + h1 { "Welcome, " ( user. display_name. clone ( ) . unwrap_or_else ( || user. username. clone ( ) ) ) } 223 + ( repo_card ( "Your repositories" , "You have no repositories yet." , & entries) ) 224 + } ) 225 + } 226 + 133 227 134 228 # [ derive ( Debug , Deserialize ) ] 135 229 pub struct LoginForm {