Expand 137 hidden lines 138 138 } ) ) 139 139 } 140 140 141 - 142 - 141 + 142 + # [ derive ( Debug , Default , Deserialize ) ] 143 + pub struct ListQuery { 144 + 145 + # [ serde ( default ) ] 146 + pub q: Option < String > , 147 + } 148 + 149 + 150 + 143 151 pub async fn user_page ( 144 152 State ( state) : State < AppState > , 145 153 MaybeUser ( viewer) : MaybeUser , 146 154 jar : CookieJar , 147 155 uri : Uri , 156 + Query ( lq) : Query < ListQuery > , 148 157 Path ( owner_name) : Path < String > , 149 158 ) -> AppResult < Response > { 150 159 let Some ( owner) = state. store. user_by_username ( & owner_name) . await ? else { Expand 2 hidden lines 153 162 let repos = visible_repos ( & state, & owner, viewer. as_ref ( ) ) . await ?; 154 163 let groups = state. store. groups_by_owner ( & owner. id) . await ?; 155 164 165 + let q = lq. q. unwrap_or_default ( ) ; 166 + let needle = q. trim ( ) . to_lowercase ( ) ; 167 + let entries: Vec < RepoEntry > = repos 168 + . iter ( ) 169 + . filter ( |r| needle. is_empty ( ) || r. path. to_lowercase ( ) . contains ( & needle) ) 170 + . map ( |r| RepoEntry :: owned ( & owner. username, r) ) 171 + . collect ( ) ; 172 + 156 173 let ( jar, chrome) = build_chrome ( & state, jar, viewer. clone ( ) , uri. path ( ) . to_string ( ) ) ; 157 174 let body = html ! { 158 - ( profile_header ( & owner) ) 159 - @if !groups. is_empty ( ) { 160 - section class="listing" { 161 - h2 { "Groups" } 162 - ul class="repo-list card" { 163 - @for group in & groups { 164 - li { 165 - a class="repo-row" href={ "/" ( owner. username) "/" ( group. path) } { 166 - span class="repo-row-name" { 167 - ( icon ( Icon :: Folder ) ) span { ( group. path) } 175 + div class="profile-layout" { 176 + aside class="profile-sidebar" { ( profile_sidebar ( & owner) ) } 177 + div class="profile-main" { 178 + form class="search-row" method="get" { 179 + input type ="search" name="q" value=( q) 180 + placeholder="Search repositories…" aria-label="Search repositories" ; 181 + button class="btn btn-primary" type ="submit" { "Search" } 182 + } 183 + @if !groups. is_empty ( ) { 184 + section class="listing" { 185 + h2 { "Groups" } 186 + ul class="repo-list card" { 187 + @for group in & groups { 188 + li { 189 + a class="repo-row" href={ "/" ( owner. username) "/" ( group. path) } { 190 + span class="repo-row-name" { 191 + ( icon ( Icon :: Folder ) ) span { ( group. path) } 192 + } 193 + } 168 194 } 169 195 } 170 196 } 171 197 } 172 198 } 199 + ( repo_card ( "Repositories" , "No repositories." , & entries) ) 173 200 } 174 201 } 175 - ( owned_repo_card ( & owner. username, & repos) ) 176 202 } ; 177 203 Ok ( ( jar, page ( & chrome, & owner. username, body) ) . into_response ( ) ) 178 204 } Expand 62 hidden lines 241 267 "branches" => branches_view ( state, viewer, jar, uri, ctx) . await , 242 268 "tags" => tags_view ( state, viewer, jar, uri, ctx) . await , 243 269 "search" => { 244 - let header = repo_header ( state, & ctx, Tab :: Search , & ctx. repo. default_branch) ; 270 + let header = repo_header ( & ctx, Tab :: Search , & ctx. repo. default_branch) ; 245 271 let raw_query = dq. q. clone ( ) . unwrap_or_default ( ) ; 246 272 crate :: search:: in_repo ( state, viewer, jar, uri, & ctx, & raw_query, header) . await 247 273 } Expand 49 hidden lines 297 323 . await ?; 298 324 let readme = load_readme ( state, & ctx. repo. id, & rev, & entries) . await ; 299 325 html ! { 300 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 326 + ( repo_header ( & ctx, Tab :: Code , & rev_name) ) 327 + ( clone_widget ( state, & ctx) ) 301 328 ( tree_table ( & ctx. owner. username, & ctx. repo. path, & rev_name, "" , & entries) ) 302 329 @if let Some ( rendered) = readme { 303 - div class="card" style="margin-top:1rem" { ( rendered) } 330 + div class="card markdown readme" { ( rendered) } 304 331 } 305 332 } 306 333 } Expand 34 hidden lines 341 368 342 369 fn empty_repo_body ( state : & AppState , ctx : & RepoCtx ) -> Markup { 343 370 html ! { 344 - ( repo_header ( state, ctx, Tab :: Code , & ctx. repo. default_branch) ) 371 + ( repo_header ( ctx, Tab :: Code , & ctx. repo. default_branch) ) 345 372 div class="card" { 346 373 h2 { "This repository is empty" } 347 374 p class="muted" { "Push a commit to get started:" } Expand 29 hidden lines 377 404 . await ?; 378 405 379 406 let body = html ! { 380 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 407 + ( repo_header ( & ctx, Tab :: Code , & rev_name) ) 381 408 ( breadcrumb ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path) ) 382 409 ( tree_table ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path, & entries) ) 383 410 } ; Expand 39 hidden lines 423 450 424 451 let content_body = render_blob ( & ctx, & rev_name, & path, & blob, attr_lang. as_deref ( ) ) ; 425 452 let body = html ! { 426 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 453 + ( repo_header ( & ctx, Tab :: Code , & rev_name) ) 427 454 ( breadcrumb ( & ctx. owner. username, & ctx. repo. path, & rev_name, & path) ) 428 455 ( content_body) 429 456 } ; Expand 117 hidden lines 547 574 ) -> AppResult < Response > { 548 575 let branches = git_read ( state, & ctx. repo. id, git:: Repo :: branches) . await ?; 549 576 let body = html ! { 550 - ( repo_header ( state, & ctx, Tab :: Branches , & ctx. repo. default_branch) ) 577 + ( repo_header ( & ctx, Tab :: Branches , & ctx. repo. default_branch) ) 551 578 h2 { "Branches" } 552 579 table class="list" { 553 580 thead { tr { th { "Branch" } th { "Ahead" } th { "Behind" } th { "Updated" } } } Expand 28 hidden lines 582 609 ) -> AppResult < Response > { 583 610 let tags = git_read ( state, & ctx. repo. id, git:: Repo :: tags) . await ?; 584 611 let body = html ! { 585 - ( repo_header ( state, & ctx, Tab :: Tags , & ctx. repo. default_branch) ) 612 + ( repo_header ( & ctx, Tab :: Tags , & ctx. repo. default_branch) ) 586 613 h2 { "Tags" } 587 614 table class="list" { 588 615 thead { tr { th { " Tag " } th { "Commit" } th { "Kind" } } } Expand 51 hidden lines 640 667 . await ?; 641 668 642 669 let body = html ! { 643 - ( repo_header ( state, & ctx, Tab :: Code , & rev_name) ) 670 + ( repo_header ( & ctx, Tab :: Code , & rev_name) ) 644 671 h2 { "Commits" } 645 672 ul class="repo-list" { 646 673 @for ( commit, sig) in commits. iter ( ) . zip ( states. iter ( ) ) { Expand 44 hidden lines 691 718 let repo_base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 692 719 693 720 let body = html ! { 694 - ( repo_header ( state, & ctx, Tab :: Code , & ctx. repo. default_branch) ) 721 + ( repo_header ( & ctx, Tab :: Code , & ctx. repo. default_branch) ) 695 722 ( commit_meta ( & detail, & sig) ) 696 723 div class="diff-toolbar" { 697 724 span { Expand 365 hidden lines 1063 1090 repo_card ( "Repositories" , "No repositories." , & entries) 1064 1091 } 1065 1092 1066 - 1067 - 1068 - fn profile_header ( owner : & User ) -> Markup { 1093 + 1094 + 1095 + fn profile_sidebar ( owner : & User ) -> Markup { 1069 1096 let name = owner. display_name. as_deref ( ) . unwrap_or ( & owner. username) ; 1070 1097 html ! { 1071 - header class="profile-head" { 1098 + div class="card profile-card" { 1072 1099 img class="avatar avatar-xl" src={ "/avatar/" ( owner. username) } alt="" ; 1073 - div class="profile-meta" { 1074 - h1 { ( name) } 1075 - p class="profile-sub muted" { 1076 - ( icon ( Icon :: User ) ) span { "User · " ( owner. username) } 1077 - @if let Some ( pronouns) = pronouns ( owner) { 1078 - span class="profile-pronouns" { ( pronouns) } 1079 - } 1100 + h1 class="profile-name" { ( name) } 1101 + p class="profile-handle muted" { 1102 + "@" ( owner. username) 1103 + @if let Some ( pronouns) = pronouns ( owner) { 1104 + " · " ( pronouns) 1080 1105 } 1081 - @if let Some ( bio) = non_empty ( owner. bio. as_ref ( ) ) { 1082 - p class="profile-bio" { ( bio) } 1083 - } 1084 - @let links = profile_links ( owner) ; 1085 - @if !links. is_empty ( ) { 1086 - ul class="profile-links muted" { 1087 - @for link in & links { ( link) } 1088 - } 1106 + } 1107 + @if let Some ( bio) = non_empty ( owner. bio. as_ref ( ) ) { 1108 + div class="profile-bio markdown" { ( markdown:: render ( bio) ) } 1109 + } 1110 + @let links = profile_links ( owner) ; 1111 + @if !links. is_empty ( ) { 1112 + ul class="profile-links muted" { 1113 + @for link in & links { ( link) } 1089 1114 } 1090 1115 } 1116 + p class="profile-joined muted" { "Joined on " ( fmt_joined ( owner. created_at) ) } 1091 1117 } 1092 1118 } 1093 1119 } Expand 61 hidden lines 1155 1181 } 1156 1182 1157 1183 1158 - fn repo_header ( state : & AppState , ctx : & RepoCtx , active : Tab , rev : & str ) -> Markup { 1184 + fn repo_header ( ctx : & RepoCtx , active : Tab , rev : & str ) -> Markup { 1159 1185 let base = format ! ( "/{}/{}" , ctx. owner. username, ctx. repo. path) ; 1160 - let tab = |t : Tab , label : & str , href : String | { 1186 + let tab = |t : Tab , label : & str , glyph : Icon , href : String | { 1161 1187 html ! { 1162 - a href=( href) aria-current=[ ( active == t) . then_some ( "page" ) ] { ( label) } 1188 + a href=( href) aria-current=[ ( active == t) . then_some ( "page" ) ] { 1189 + ( icon ( glyph) ) span { ( label) } 1190 + } 1163 1191 } 1164 1192 } ; 1165 1193 html ! { Expand 5 hidden lines 1171 1199 } 1172 1200 @if let Some ( desc) = & ctx. repo. description { p class="muted" { ( desc) } } 1173 1201 nav class="tabs repo-tabs" { 1174 - ( tab ( Tab :: Code , "Code" , base. clone ( ) ) ) 1175 - ( tab ( Tab :: Branches , "Branches" , format ! ( "{base}/-/branches" ) ) ) 1176 - ( tab ( Tab :: Tags , "Tags" , format ! ( "{base}/-/tags" ) ) ) 1177 - ( tab ( Tab :: Search , "Search" , format ! ( "{base}/-/search" ) ) ) 1202 + ( tab ( Tab :: Code , "Code" , Icon :: File , base. clone ( ) ) ) 1203 + ( tab ( Tab :: Branches , "Branches" , Icon :: GitBranch , format ! ( "{base}/-/branches" ) ) ) 1204 + ( tab ( Tab :: Tags , "Tags" , Icon :: Box , format ! ( "{base}/-/tags" ) ) ) 1205 + ( tab ( Tab :: Search , "Search" , Icon :: Search , format ! ( "{base}/-/search" ) ) ) 1178 1206 } 1179 1207 div class="ref-row" { 1180 1208 1181 1209 a class="badge" href=( format ! ( "{base}/-/branches" ) ) { ( rev) } 1182 1210 a href=( format ! ( "{base}/-/commits/{rev}" ) ) { "Commits" } 1183 1211 } 1184 - div class="clone-row" { 1185 - input type ="text" readonly value=( clone_https ( state, ctx) ) 1186 - aria-label="HTTPS clone URL" ; 1187 - button class="btn" type ="button" data-clipboard=( clone_https ( state, ctx) ) { 1188 - ( icon ( Icon :: Copy ) ) span { "Copy" } 1189 - } 1212 + } 1213 + } 1214 + } 1215 + 1216 + 1217 + fn clone_widget ( state : & AppState , ctx : & RepoCtx ) -> Markup { 1218 + html ! { 1219 + div class="clone-row" { 1220 + span class="muted clone-label" { "Clone" } 1221 + input type ="text" readonly value=( clone_https ( state, ctx) ) 1222 + aria-label="HTTPS clone URL" ; 1223 + button class="btn" type ="button" data-clipboard=( clone_https ( state, ctx) ) { 1224 + ( icon ( Icon :: Copy ) ) span { "Copy" } 1190 1225 } 1191 1226 } 1192 1227 } Expand 57 hidden lines 1250 1285 } ; 1251 1286 format ! ( "/{owner}/{repo_path}/-/{view}/{rev}/{sub}" ) 1252 1287 } ; 1288 + 1289 + let mut sorted: Vec < & git:: TreeEntry > = entries. iter ( ) . collect ( ) ; 1290 + sorted. sort_by ( |a, b| { 1291 + let is_file = |e : & git:: TreeEntry | e. kind != git:: EntryKind :: Directory ; 1292 + is_file ( a) 1293 + . cmp ( & is_file ( b) ) 1294 + . then_with ( || a. name. to_lowercase ( ) . cmp ( & b. name. to_lowercase ( ) ) ) 1295 + } ) ; 1253 1296 html ! { 1254 - table class="list tree" { 1255 - tbody { 1256 - @for entry in entries { 1257 - tr { 1258 - td class="tree-icon" { ( icon ( entry_icon ( entry. kind) ) ) } 1259 - td { a href=( href ( & entry. name, entry. kind) ) { ( entry. name) } } 1260 - } 1297 + div class="file-tree card" { 1298 + @for entry in sorted { 1299 + @let is_dir = entry. kind == git:: EntryKind :: Directory ; 1300 + a class=( if is_dir { "file-row is-dir" } else { "file-row" } ) 1301 + href=( href ( & entry. name, entry. kind) ) { 1302 + span class="file-icon" { ( icon ( entry_icon ( entry. kind) ) ) } 1303 + span class="file-name" { ( entry. name) } 1261 1304 } 1262 1305 } 1263 1306 } Expand 55 hidden lines 1319 1362 ) 1320 1363 } 1321 1364 1365 + 1366 + 1367 + fn fmt_joined ( ms : i64 ) -> String { 1368 + let secs = ms. div_euclid ( 1000 ) ; 1369 + match time:: OffsetDateTime :: from_unix_timestamp ( secs) { 1370 + Ok ( dt) => { 1371 + let month = match dt. month ( ) { 1372 + time:: Month :: January => "Jan" , 1373 + time:: Month :: February => "Feb" , 1374 + time:: Month :: March => "Mar" , 1375 + time:: Month :: April => "Apr" , 1376 + time:: Month :: May => "May" , 1377 + time:: Month :: June => "Jun" , 1378 + time:: Month :: July => "Jul" , 1379 + time:: Month :: August => "Aug" , 1380 + time:: Month :: September => "Sep" , 1381 + time:: Month :: October => "Oct" , 1382 + time:: Month :: November => "Nov" , 1383 + time:: Month :: December => "Dec" , 1384 + } ; 1385 + format ! ( "{month} {}, {}" , dt. day ( ) , dt. year ( ) ) 1386 + } 1387 + Err ( _) => "—" . to_string ( ) , 1388 + } 1389 + } 1390 + 1322 1391 1323 1392 fn fmt_date ( ms : i64 ) -> String { 1324 1393 let secs = ms. div_euclid ( 1000 ) ;