feat(web): completely overhauled user profile page
This commit is contained in:
parent
45a69f0332
commit
60cf700b95
3 changed files with 126 additions and 44 deletions
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
)}
|
||||
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});")}
|
||||
>
|
||||
{
|
||||
dissolve::strip_html_tags(x.summary().unwrap_or(""))
|
||||
.into_iter()
|
||||
.map(|x| view! { <p>{x}</p> })
|
||||
.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>
|
||||
// <table class="align w-100">
|
||||
// <tr><td rowspan=3>
|
||||
// <img src=
|
||||
|
||||
// </table>
|
||||
<div style="height: 10em"></div>
|
||||
</div>
|
||||
<hr />
|
||||
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", x.id().unwrap_or_default()))) />
|
||||
}.into_view(),
|
||||
<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! { <div>{x}</div> })
|
||||
.collect_view()
|
||||
}</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", &id.clone()))) />
|
||||
}.into_view()
|
||||
},
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue