| | @@ -38,6 +38,7 @@ use super::{ |
| 38 | 38 | context::{ |
| 39 | 39 | current_actor, location, memberships, orgs, public_origin, repos, server_error, storage, |
| 40 | 40 | }, |
| 41 | + layout::{narrow, wide}, |
| 41 | 42 | markdown, |
| 42 | 43 | profile::profile_for, |
| 43 | 44 | }; |
| | @@ -178,82 +179,84 @@ async fn repo_page(cx: &Cx) -> Result { |
| 178 | 179 | let browsed = browsed_at(cx, &repo, None, &RepoPath::root()).await?; |
| 179 | 180 | |
| 180 | 181 | view! { |
| 181 | | − <header class="mb-8"> |
| 182 | | − <p class="font-mono text-sm text-muted-foreground"> |
| 183 | | − <a href=(format!("/{}", repo.handle)) class="hover:text-foreground"> |
| 184 | | − "@" (repo.handle.as_str()) |
| 185 | | − </a> |
| 186 | | − " / " |
| 187 | | − </p> |
| 188 | | − <div class="mt-1 flex items-center gap-3"> |
| 189 | | − <h1 class="text-2xl font-semibold tracking-tight">(repo.name.as_str())</h1> |
| 190 | | − if !repo.visibility.is_public() { |
| 191 | | − badge(variant: BadgeVariant::Outline, "Private") |
| 192 | | − } |
| 193 | | − // Only the owner can change a repository, so only the owner is offered |
| 194 | | − // the way in. The settings page enforces this again — this is the link |
| 195 | | − // not being a dead end, not the authorization. |
| 196 | | − if repo.viewer_is_owner { |
| 197 | | − <a |
| 198 | | − href=(format!("/{}/repos/{}/settings", repo.handle, repo.name)) |
| 199 | | − class=(format!( |
| 200 | | − "ml-auto {}", |
| 201 | | − button_variants(ButtonVariant::Outline, ButtonSize::Sm) |
| 202 | | − )) |
| 203 | | − >"Settings"</a> |
| 204 | | − } |
| 205 | | − </div> |
| 206 | | − ({ |
| 207 | | − match &repo.description { |
| 208 | | − Some(description) => view! { |
| 209 | | − <p class="mt-3 text-sm leading-relaxed">(description)</p> |
| 210 | | − }, |
| 211 | | − None => view! {}, |
| 212 | | − } |
| 213 | | − }?) |
| 214 | | − </header> |
| 215 | | − |
| 216 | | − clone_url(url: clone.as_str()) |
| 217 | | − |
| 218 | | − match &browsed { |
| 219 | | − Browsed::Empty => empty_repo(url: clone.as_str()), |
| 220 | | − Browsed::Directory { rev, path, entries } => { |
| 221 | | − <div class="mt-8 mb-3 flex items-center justify-between text-sm"> |
| 222 | | − <span class="inline-flex items-center gap-1.5 font-mono text-xs text-muted-foreground"> |
| 223 | | − (rev.as_str()) |
| 224 | | − </span> |
| 225 | | − <a |
| 226 | | − href=(format!("/{}/repos/{}/log", repo.handle, repo.name)) |
| 227 | | − class="text-muted-foreground hover:text-foreground" |
| 228 | | − >"Commits"</a> |
| 182 | + wide( |
| 183 | + <header class="mb-8"> |
| 184 | + <p class="font-mono text-sm text-muted-foreground"> |
| 185 | + <a href=(format!("/{}", repo.handle)) class="hover:text-foreground"> |
| 186 | + "@" (repo.handle.as_str()) |
| 187 | + </a> |
| 188 | + " / " |
| 189 | + </p> |
| 190 | + <div class="mt-1 flex items-center gap-3"> |
| 191 | + <h1 class="text-2xl font-semibold tracking-tight">(repo.name.as_str())</h1> |
| 192 | + if !repo.visibility.is_public() { |
| 193 | + badge(variant: BadgeVariant::Outline, "Private") |
| 194 | + } |
| 195 | + // Only the owner can change a repository, so only the owner is offered |
| 196 | + // the way in. The settings page enforces this again — this is the link |
| 197 | + // not being a dead end, not the authorization. |
| 198 | + if repo.viewer_is_owner { |
| 199 | + <a |
| 200 | + href=(format!("/{}/repos/{}/settings", repo.handle, repo.name)) |
| 201 | + class=(format!( |
| 202 | + "ml-auto {}", |
| 203 | + button_variants(ButtonVariant::Outline, ButtonSize::Sm) |
| 204 | + )) |
| 205 | + >"Settings"</a> |
| 206 | + } |
| 229 | 207 | </div> |
| 230 | | − directory( |
| 231 | | − handle: repo.handle.as_str(), |
| 232 | | − name: repo.name.as_str(), |
| 233 | | − rev: rev, |
| 234 | | − path: path, |
| 235 | | − entries: entries, |
| 236 | | − ) |
| 237 | | − |
| 238 | | − // No README means nothing here at all — an empty panel saying a |
| 239 | | − // repository has no README is worse than the silence. |
| 240 | | − match readme_of(entries) { |
| 241 | | − Some(entry) => readme_card(repo: &repo, rev: rev, entry: entry), |
| 242 | | − None => "", |
| 243 | | − } |
| 244 | | − }, |
| 245 | | − // The root of a revision is always a directory, so this is unreachable in |
| 246 | | − // practice — rendered rather than errored so it can never be a 500. |
| 247 | | − Browsed::File { rev, path, file } => <div class="mt-8"> |
| 248 | | − blob( |
| 249 | | − handle: repo.handle.as_str(), |
| 250 | | − name: repo.name.as_str(), |
| 251 | | − rev: rev, |
| 252 | | − path: path, |
| 253 | | − file: file, |
| 254 | | − ) |
| 255 | | − </div>, |
| 256 | | − } |
| 208 | + ({ |
| 209 | + match &repo.description { |
| 210 | + Some(description) => view! { |
| 211 | + <p class="mt-3 text-sm leading-relaxed">(description)</p> |
| 212 | + }, |
| 213 | + None => view! {}, |
| 214 | + } |
| 215 | + }?) |
| 216 | + </header> |
| 217 | + |
| 218 | + clone_url(url: clone.as_str()) |
| 219 | + |
| 220 | + match &browsed { |
| 221 | + Browsed::Empty => empty_repo(url: clone.as_str()), |
| 222 | + Browsed::Directory { rev, path, entries } => { |
| 223 | + <div class="mt-8 mb-3 flex items-center justify-between text-sm"> |
| 224 | + <span class="inline-flex items-center gap-1.5 font-mono text-xs text-muted-foreground"> |
| 225 | + (rev.as_str()) |
| 226 | + </span> |
| 227 | + <a |
| 228 | + href=(format!("/{}/repos/{}/log", repo.handle, repo.name)) |
| 229 | + class="text-muted-foreground hover:text-foreground" |
| 230 | + >"Commits"</a> |
| 231 | + </div> |
| 232 | + directory( |
| 233 | + handle: repo.handle.as_str(), |
| 234 | + name: repo.name.as_str(), |
| 235 | + rev: rev, |
| 236 | + path: path, |
| 237 | + entries: entries, |
| 238 | + ) |
| 239 | + |
| 240 | + // No README means nothing here at all — an empty panel saying a |
| 241 | + // repository has no README is worse than the silence. |
| 242 | + match readme_of(entries) { |
| 243 | + Some(entry) => readme_card(repo: &repo, rev: rev, entry: entry), |
| 244 | + None => "", |
| 245 | + } |
| 246 | + }, |
| 247 | + // The root of a revision is always a directory, so this is unreachable in |
| 248 | + // practice — rendered rather than errored so it can never be a 500. |
| 249 | + Browsed::File { rev, path, file } => <div class="mt-8"> |
| 250 | + blob( |
| 251 | + handle: repo.handle.as_str(), |
| 252 | + name: repo.name.as_str(), |
| 253 | + rev: rev, |
| 254 | + path: path, |
| 255 | + file: file, |
| 256 | + ) |
| 257 | + </div>, |
| 258 | + } |
| 259 | + ) |
| 257 | 260 | } |
| 258 | 261 | } |
| 259 | 262 | |
| | @@ -397,71 +400,73 @@ async fn new_repo_form( |
| 397 | 400 | error: &str, |
| 398 | 401 | ) -> Result { |
| 399 | 402 | view! { |
| 400 | | − <h1 class="text-xl font-semibold tracking-tight">"New repository"</h1> |
| 401 | | − <p class="mt-1 font-mono text-sm text-muted-foreground">"@" (handle)</p> |
| 402 | | − |
| 403 | | − if !error.is_empty() { |
| 404 | | − <div class="mt-6"> |
| 405 | | − flash(kind: FlashKind::Error, (error)) |
| 406 | | − </div> |
| 407 | | − } |
| 403 | + narrow( |
| 404 | + <h1 class="text-xl font-semibold tracking-tight">"New repository"</h1> |
| 405 | + <p class="mt-1 font-mono text-sm text-muted-foreground">"@" (handle)</p> |
| 408 | 406 | |
| 409 | | − <form method="post" action=(format!("/{handle}/repos/new")) class="mt-6 space-y-5"> |
| 410 | | − <div class="space-y-2"> |
| 411 | | − label(attrs: attributes! { for="name" }, "Name") |
| 412 | | − input(attrs: attributes! { |
| 413 | | − id="name" |
| 414 | | − name="name" |
| 415 | | − type="text" |
| 416 | | − value=(name) |
| 417 | | − placeholder="my-project" |
| 418 | | − required=(true) |
| 419 | | − maxlength=(RepoName::MAX_LEN.to_string()) |
| 420 | | − autofocus=(true) |
| 421 | | − }) |
| 422 | | − <p class="text-xs text-muted-foreground"> |
| 423 | | − "Letters, digits, hyphens, underscores and dots. Lowercased." |
| 424 | | − </p> |
| 425 | | − </div> |
| 407 | + if !error.is_empty() { |
| 408 | + <div class="mt-6"> |
| 409 | + flash(kind: FlashKind::Error, (error)) |
| 410 | + </div> |
| 411 | + } |
| 412 | + |
| 413 | + <form method="post" action=(format!("/{handle}/repos/new")) class="mt-6 space-y-5"> |
| 414 | + <div class="space-y-2"> |
| 415 | + label(attrs: attributes! { for="name" }, "Name") |
| 416 | + input(attrs: attributes! { |
| 417 | + id="name" |
| 418 | + name="name" |
| 419 | + type="text" |
| 420 | + value=(name) |
| 421 | + placeholder="my-project" |
| 422 | + required=(true) |
| 423 | + maxlength=(RepoName::MAX_LEN.to_string()) |
| 424 | + autofocus=(true) |
| 425 | + }) |
| 426 | + <p class="text-xs text-muted-foreground"> |
| 427 | + "Letters, digits, hyphens, underscores and dots. Lowercased." |
| 428 | + </p> |
| 429 | + </div> |
| 426 | 430 | |
| 427 | | − <div class="space-y-2"> |
| 428 | | − label(attrs: attributes! { for="description" }, "Description") |
| 429 | | − textarea( |
| 430 | | − attrs: attributes! { |
| 431 | | − id="description" |
| 432 | | − name="description" |
| 433 | | − rows="2" |
| 434 | | − maxlength=(Repository::MAX_DESCRIPTION_LEN.to_string()) |
| 435 | | − placeholder="A sentence for your profile." |
| 436 | | − }, |
| 437 | | − (description) |
| 438 | | − ) |
| 439 | | − <p class="text-xs text-muted-foreground"> |
| 440 | | − "Optional. At most " |
| 441 | | − (Repository::MAX_DESCRIPTION_LEN.to_string()) " characters." |
| 442 | | − </p> |
| 443 | | − </div> |
| 431 | + <div class="space-y-2"> |
| 432 | + label(attrs: attributes! { for="description" }, "Description") |
| 433 | + textarea( |
| 434 | + attrs: attributes! { |
| 435 | + id="description" |
| 436 | + name="description" |
| 437 | + rows="2" |
| 438 | + maxlength=(Repository::MAX_DESCRIPTION_LEN.to_string()) |
| 439 | + placeholder="A sentence for your profile." |
| 440 | + }, |
| 441 | + (description) |
| 442 | + ) |
| 443 | + <p class="text-xs text-muted-foreground"> |
| 444 | + "Optional. At most " |
| 445 | + (Repository::MAX_DESCRIPTION_LEN.to_string()) " characters." |
| 446 | + </p> |
| 447 | + </div> |
| 444 | 448 | |
| 445 | | − <div class="space-y-2"> |
| 446 | | − label(attrs: attributes! { for="visibility" }, "Visibility") |
| 447 | | − select( |
| 448 | | − attrs: attributes! { id="visibility" name="visibility" }, |
| 449 | | − <option value="public" selected=(visibility.is_public())>"Public"</option> |
| 450 | | − <option value="private" selected=(!visibility.is_public())>"Private"</option> |
| 451 | | − ) |
| 452 | | − <p class="text-xs text-muted-foreground"> |
| 453 | | − "Public repositories appear on your profile to anyone." |
| 454 | | − </p> |
| 455 | | − </div> |
| 449 | + <div class="space-y-2"> |
| 450 | + label(attrs: attributes! { for="visibility" }, "Visibility") |
| 451 | + select( |
| 452 | + attrs: attributes! { id="visibility" name="visibility" }, |
| 453 | + <option value="public" selected=(visibility.is_public())>"Public"</option> |
| 454 | + <option value="private" selected=(!visibility.is_public())>"Private"</option> |
| 455 | + ) |
| 456 | + <p class="text-xs text-muted-foreground"> |
| 457 | + "Public repositories appear on your profile to anyone." |
| 458 | + </p> |
| 459 | + </div> |
| 456 | 460 | |
| 457 | | − <div class="flex items-center gap-3"> |
| 458 | | − button(attrs: attributes! { type="submit" }, "Create repository") |
| 459 | | − <a |
| 460 | | − href=(format!("/{handle}")) |
| 461 | | − class="text-sm text-muted-foreground hover:text-foreground" |
| 462 | | − >"Cancel"</a> |
| 463 | | − </div> |
| 464 | | − </form> |
| 461 | + <div class="flex items-center gap-3"> |
| 462 | + button(attrs: attributes! { type="submit" }, "Create repository") |
| 463 | + <a |
| 464 | + href=(format!("/{handle}")) |
| 465 | + class="text-sm text-muted-foreground hover:text-foreground" |
| 466 | + >"Cancel"</a> |
| 467 | + </div> |
| 468 | + </form> |
| 469 | + ) |
| 465 | 470 | } |
| 466 | 471 | } |
| 467 | 472 | |