steid

@jamesgill /

a69e380feat: public profile page at /{handle}1mo
1//! The profile page — `/{handle}`.
2//!
3//! Public. This is the first page that renders for anonymous visitors, so everything it
4//! shows comes from [`PublicProfile`](crate::application::PublicProfile), which has no
5//! private fields to leak.
6
7use topcoat::{
8 Result,
9 context::Cx,
10 router::{
11 error::{RouterErrorExt, not_found},
12 page, path_param,
13 },
14 view::view,
15};
16
17use crate::{
0c5ca49feat: list repositories on the profile8d
18 application::{PublicProfile, list_repos, view_profile},
af126c7feat: tailwind theme and the first components25d
19 components::button::{ButtonSize, ButtonVariant, button_variants},
a69e380feat: public profile page at /{handle}1mo
20 domain::OrgName,
21};
22
0c5ca49feat: list repositories on the profile8d
23use super::{
24 context::{current_actor, memberships, orgs, repos, server_error},
25 repo::repo_list,
26};
a69e380feat: public profile page at /{handle}1mo
27
28/// `{handle}` from the path. The struct name snake-cased is the parameter name.
29///
30/// Declared as `str` so the raw segment arrives unparsed — validation is `OrgName`'s
31/// job, and a handle that fails it is a page that does not exist rather than a bad
32/// request.
33#[path_param]
34struct Handle(str);
35
8622a05feat: list a handle's repositories over /api8d
36/// Resolves `{handle}` from the path, or 404.
a69e380feat: public profile page at /{handle}1mo
37///
38/// A malformed handle 404s rather than erroring: `/Not A Handle` is a page that does
39/// not exist, not a bad request.
8622a05feat: list a handle's repositories over /api8d
40///
41/// Shared with `/api`, which resolves the same segment without wanting a profile —
42/// listing repositories under a handle needs the handle and nothing else.
43pub(super) fn handle_param(cx: &Cx) -> Result<OrgName> {
44 Ok(OrgName::new(path_param::<Handle>(cx)).map_err(|_| not_found())?)
45}
46
47/// Resolves `{handle}` from the path into a profile, or 404.
a69e380feat: public profile page at /{handle}1mo
48pub(super) async fn profile_for(cx: &Cx) -> Result<PublicProfile> {
8622a05feat: list a handle's repositories over /api8d
49 let handle = handle_param(cx)?;
a69e380feat: public profile page at /{handle}1mo
50 let actor = current_actor(cx).await?;
51
52 Ok(view_profile(&handle, &actor, &orgs(cx), &memberships(cx))
53 .await
54 .map_err(server_error)?
55 .ok_or_not_found()?)
56}
57
58#[page("/{handle}")]
59async fn profile(cx: &Cx) -> Result {
60 let profile = profile_for(cx).await?;
61
0c5ca49feat: list repositories on the profile8d
62 // The handle resolved above, so `None` here is impossible; an empty list is the
63 // right answer either way rather than a 500.
64 let listed = list_repos(
65 &profile.handle,
66 &current_actor(cx).await?,
67 &orgs(cx),
68 &memberships(cx),
69 &repos(cx),
70 )
71 .await
72 .map_err(server_error)?
73 .unwrap_or_default();
74
a69e380feat: public profile page at /{handle}1mo
75 view! {
af126c7feat: tailwind theme and the first components25d
76 <header class="mb-10">
77 <h1 class="text-2xl font-semibold tracking-tight">(&profile.label)</h1>
78 <p class="mt-1 font-mono text-sm text-muted-foreground">
79 "@" (profile.handle.as_str())
80 </p>
a69e380feat: public profile page at /{handle}1mo
81 ({
82 match &profile.bio {
af126c7feat: tailwind theme and the first components25d
83 Some(bio) => view! { <p class="mt-3 text-sm leading-relaxed">(bio)</p> },
a69e380feat: public profile page at /{handle}1mo
84 None => view! {},
85 }
86 }?)
87 ({
88 if profile.viewer_is_owner {
89 view! {
af126c7feat: tailwind theme and the first components25d
90 <p class="mt-4">
91 <a
92 href=(format!("/{}/settings", profile.handle))
93 class=(button_variants(ButtonVariant::Outline, ButtonSize::Sm))
94 >"Edit profile"</a>
95 </p>
a69e380feat: public profile page at /{handle}1mo
96 }
97 } else {
98 view! {}
99 }
100 }?)
101 </header>
102
af126c7feat: tailwind theme and the first components25d
103 <section class="mt-8">
285f5fdfeat: create and view repositories through the browser24d
104 <div class="flex items-center justify-between">
105 <h2 class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
106 "Repositories"
107 </h2>
108 if profile.viewer_is_owner {
109 <a
110 href=(format!("/{}/repos/new", profile.handle))
111 class=(button_variants(ButtonVariant::Outline, ButtonSize::Sm))
112 >"New repository"</a>
113 }
114 </div>
0c5ca49feat: list repositories on the profile8d
115 repo_list(handle: profile.handle.as_str(), repos: &listed)
a69e380feat: public profile page at /{handle}1mo
116 </section>
117
af126c7feat: tailwind theme and the first components25d
118 <section class="mt-8">
119 <h2 class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
120 "Writing"
121 </h2>
122 <p class="mt-2 rounded-lg border border-border px-4 py-6 text-center text-sm text-muted-foreground">
123 "Nothing here yet."
124 </p>
a69e380feat: public profile page at /{handle}1mo
125 </section>
126
af126c7feat: tailwind theme and the first components25d
127 <section class="mt-8">
128 <h2 class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
129 "Projects"
130 </h2>
131 <p class="mt-2 rounded-lg border border-border px-4 py-6 text-center text-sm text-muted-foreground">
132 "Nothing here yet."
133 </p>
a69e380feat: public profile page at /{handle}1mo
134 </section>
135 }
136}