1
0
Fork 0
forked from alemi/upub

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

View file

@ -34,7 +34,9 @@ pub fn ConfigPage() -> impl IntoView {
pub fn UserPage() -> impl IntoView { pub fn UserPage() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let auth = use_context::<Auth>().expect("missing auth context"); 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 { async move {
match CACHE.get(&Uri::full("users", &id)) { match CACHE.get(&Uri::full("users", &id)) {
Some(x) => Some(x.clone()), Some(x) => Some(x.clone()),
@ -53,33 +55,70 @@ pub fn UserPage() -> impl IntoView {
{move || match actor.get() { {move || match actor.get() {
None => view! { <p>loading...</p> }.into_view(), None => view! { <p>loading...</p> }.into_view(),
Some(None) => view! { <p><code>error loading</code></p> }.into_view(), Some(None) => view! { <p><code>error loading</code></p> }.into_view(),
Some(Some(x)) => view! { Some(Some(object)) => {
<div class="ml-3 mr-3 mt-3"> let uid = object.id().unwrap_or_default().to_string();
<ActorBanner object=x.clone() /> let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
<p let background_url = object.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default();
class="pb-2 pt-2 pr-2 pl-2" let display_name = object.name().unwrap_or_default().to_string();
style={format!( let username = object.preferred_username().unwrap_or_default().to_string();
"background-image: url({}); background-size: cover;", let summary = object.summary().unwrap_or_default().to_string();
x.image().get().map(|x| x.url().id().unwrap_or_default()).unwrap_or_default() 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">
dissolve::strip_html_tags(x.summary().unwrap_or("")) // <tr><td rowspan=3>
.into_iter() // <img src=
.map(|x| view! { <p>{x}</p> })
.collect_view() // </table>
} <div style="height: 10em"></div>
</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> </div>
<hr /> <div class="overlap">
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", x.id().unwrap_or_default()))) /> <table class="pl-2 pr-2 align w-100" style="table-layout: fixed">
}.into_view(), <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! { <div>{x}</div> })
.collect_view()
}</blockquote>
</div>
</div>
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", &id.clone()))) />
}.into_view()
},
}} }}
</div> </div>
</div> </div>

View file

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