feat(web): completely overhauled user profile page

This commit is contained in:
əlemi 2024-04-18 03:34:41 +02:00
parent 45a69f0332
commit 60cf700b95
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 126 additions and 44 deletions

View file

@ -18,7 +18,7 @@
<link crossorigin rel="stylesheet" href="https://cdn.alemi.dev/web/alemi.css">
<style>
:root {
--main-col-percentage: 70%;
--main-col-percentage: 75%;
}
@font-face {
font-family: 'Fira Code';
@ -46,6 +46,7 @@
font-size: 11pt;
}
nav {
z-index: 90;
top: 0;
position: sticky;
padding-top: .05em;
@ -63,20 +64,26 @@
main {
margin: 0em 1em;
}
blockquote {
blockquote.tl {
margin-left: 1.25em;
padding-left: .3em;
}
blockquote.tl p {
margin: 0 .5em;
}
blockquote {
margin-left: .5em;
padding-left: .5em;
margin-top: 0;
margin-bottom: 0;
}
blockquote p {
margin: .5em 1em;
}
span.footer {
padding: .1em;
font-size: .6em;
color: var(--secondary);
}
hr.sep {
z-index: 100;
margin-top: 0;
padding-top: 0;
margin-bottom: 0;
@ -86,6 +93,7 @@
position: sticky;
}
div.sticky {
z-index: 100;
top: 2rem;
position: sticky;
background-color: var(--background);
@ -117,11 +125,28 @@
font-weight: bold;
color: var(--primary);
}
b.big {
font-size: 18pt;
}
div.banner {
margin-top: .3em;
outline: .3em solid #bf616a55;
}
div.overlap {
position: relative;
bottom: 3em;
}
img.avatar-circle {
display: inline;
max-height: 2em;
border-radius: 50%;
}
img.avatar-border {
background-color: var(--background);
border: .3em solid #BF616A;
}
img.inline-avatar {
max-height: 2em;
}
div.tl-header {
background-color: #bf616a55;
color: #bf616a;
@ -140,6 +165,9 @@
td.top {
vertical-align: top;
}
td.bottom {
vertical-align: bottom;
}
input[type="submit"].active {
background-color: var(--accent);
border-color: var(--accent);

View file

@ -34,7 +34,9 @@ pub fn ConfigPage() -> impl IntoView {
pub fn UserPage() -> impl IntoView {
let params = use_params_map();
let auth = use_context::<Auth>().expect("missing auth context");
let actor = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |id| {
let id = params.get().get("id").cloned().unwrap_or_default();
let _id = id.clone(); // wtf triple clone??? TODO!!
let actor = create_local_resource(move || _id.clone(), move |id| {
async move {
match CACHE.get(&Uri::full("users", &id)) {
Some(x) => Some(x.clone()),
@ -53,33 +55,70 @@ pub fn UserPage() -> impl IntoView {
{move || match actor.get() {
None => view! { <p>loading...</p> }.into_view(),
Some(None) => view! { <p><code>error loading</code></p> }.into_view(),
Some(Some(x)) => view! {
<div class="ml-3 mr-3 mt-3">
<ActorBanner object=x.clone() />
<p
class="pb-2 pt-2 pr-2 pl-2"
style={format!(
"background-image: url({}); background-size: cover;",
x.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default()
)}
>
{
dissolve::strip_html_tags(x.summary().unwrap_or(""))
Some(Some(object)) => {
let uid = object.id().unwrap_or_default().to_string();
let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
let background_url = object.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
let display_name = object.name().unwrap_or_default().to_string();
let username = object.preferred_username().unwrap_or_default().to_string();
let summary = object.summary().unwrap_or_default().to_string();
let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string();
let actor_type = object.actor_type().unwrap_or(apb::ActorType::Person);
let actor_type_tag = if actor_type == apb::ActorType::Person { None } else {
Some(view! { <sup class="ml-s"><small>"["{actor_type.as_ref().to_lowercase()}"]"</small></sup> } )
};
let created = object.published();
let following = object.following().get().map(|x| x.total_items().unwrap_or(0)).unwrap_or_default();
let followers = object.followers().get().map(|x| x.total_items().unwrap_or(0)).unwrap_or_default();
view! {
<div class="ml-3 mr-3">
<div
class="banner"
style={format!("background: center / cover url({background_url});")}
>
// <table class="align w-100">
// <tr><td rowspan=3>
// <img src=
// </table>
<div style="height: 10em"></div>
</div>
<div class="overlap">
<table class="pl-2 pr-2 align w-100" style="table-layout: fixed">
<tr>
<td rowspan=4 style="width: 8em">
<img class="avatar-circle avatar-border mr-s" src={avatar_url} style="height: 7em; width: 7em"/>
</td>
<td rowspan=2 class="bottom">
<b class="big">{display_name}</b>{actor_type_tag}
</td>
<td rowspan=2 class="bottom rev"><span class="emoji" title="statuses">"\u{1f582}"</span>" : "0</td>
</tr>
<tr></tr>
<tr>
<td class="top">
<small><a class="clean hover" href={uid} target="_blank">{username.clone()}@{domain}</a></small>
</td>
<td class="rev"><span class="emoji" title="following">"👥"</span>" : "{following}</td>
</tr>
<tr>
<td>
<DateTime t=created />
</td>
<td class="rev"><span class="emoji" title="followers">"📢"</span>" : "{followers}</td>
</tr>
</table>
<blockquote class="ml-2 mt-1">{
dissolve::strip_html_tags(&summary)
.into_iter()
.map(|x| view! { <p>{x}</p> })
.map(|x| view! { <div>{x}</div> })
.collect_view()
}
</p>
<ul>
<li><code>type</code>" "<b>{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}</b></li>
<li><code>following</code>" "<b>{x.following().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
<li><code>followers</code>" "<b>{x.followers().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
<li><code>created</code>" "{x.published().map(|x| x.to_rfc3339())}</li>
</ul>
</div>
<hr />
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", x.id().unwrap_or_default()))) />
}.into_view(),
}</blockquote>
</div>
</div>
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", &id.clone()))) />
}.into_view()
},
}}
</div>
</div>

View file

@ -22,16 +22,14 @@ pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView {
} else {
"🔗"
};
let date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_else(||
activity.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default()
);
let date = object.published().or(activity.published());
let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
view! {
<div>
<table class="align w-100" >
<tr>
<td rowspan="2" >
<ActorBanner object=actor />
<ActorBanner object=actor tiny=true />
</td>
<td class="rev" >
<code class="color moreinfo" title={object_id.clone()} >{kind.as_ref().to_string()}</code>
@ -41,7 +39,7 @@ pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView {
<tr>
<td class="rev">
<a class="hover" href={Uri::web("objects", &object_id)} >
<small>{date}</small>
<DateTime t=date />
</a>
</td>
</tr>
@ -56,7 +54,11 @@ pub fn InlineActivity(activity: serde_json::Value) -> impl IntoView {
}
#[component]
pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
pub fn ActorBanner(
object: serde_json::Value,
#[prop(optional)]
tiny: bool
) -> impl IntoView {
match object {
serde_json::Value::String(id) => view! {
<div><b>{id}</b></div>
@ -72,7 +74,7 @@ pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
<div>
<table class="align" >
<tr>
<td rowspan="2" ><a href={uri.clone()} ><img class="avatar-circle" src={avatar_url} /></a></td>
<td rowspan="2" ><a href={uri.clone()} ><img class="avatar-circle" class:inline-avatar=move|| tiny src={avatar_url} /></a></td>
<td><b>{display_name}</b></td>
</tr>
<tr>
@ -92,8 +94,7 @@ pub fn ActorBanner(object: serde_json::Value) -> impl IntoView {
pub fn Object(object: serde_json::Value) -> impl IntoView {
let summary = object.summary().unwrap_or_default().to_string();
let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
let date = object.published().map(|x| x.format("%Y/%m/%d %H:%M:%S").to_string()).unwrap_or_default();
let date_rfc = object.published().map(|x| x.to_rfc3339()).unwrap_or_default();
let date = object.published();
let author_id = object.attributed_to().id().unwrap_or_default();
let author = CACHE.get(&author_id).unwrap_or(serde_json::Value::String(author_id.clone()));
view! {
@ -114,8 +115,8 @@ pub fn Object(object: serde_json::Value) -> impl IntoView {
}</td>
</tr>
<tr class="post-table" >
<td class="post-table pa-1" ><ActorBanner object=author /></td>
<td class="post-table pa-1 center" ><small title={date_rfc} >{date}</small></td>
<td class="post-table pa-1" ><ActorBanner object=author tiny=true /></td>
<td class="post-table pa-1 center" ><DateTime t=date /></td>
</tr>
</table>
</div>
@ -128,8 +129,22 @@ pub fn ObjectInline(object: serde_json::Value) -> impl IntoView {
let content = dissolve::strip_html_tags(object.content().unwrap_or_default());
view! {
{if summary.is_empty() { None } else { Some(view! { <code class="color">{summary}</code> })}}
<blockquote>
<blockquote class="tl">
{content.into_iter().map(|x| view! { <p>{x}</p> }).collect_view()}
</blockquote>
}
}
#[component]
pub fn DateTime(t: Option<chrono::DateTime<chrono::Utc>>) -> impl IntoView {
match t {
Some(t) => {
let pretty = t.format("%Y/%m/%d %H:%M:%S").to_string();
let rfc = t.to_rfc3339();
Some(view! {
<small title={rfc}>{pretty}</small>
})
},
None => None,
}
}