forked from alemi/upub
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">
|
<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);
|
||||||
|
|
|
@ -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> } )
|
||||||
dissolve::strip_html_tags(x.summary().unwrap_or(""))
|
};
|
||||||
|
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()
|
.into_iter()
|
||||||
.map(|x| view! { <p>{x}</p> })
|
.map(|x| view! { <div>{x}</div> })
|
||||||
.collect_view()
|
.collect_view()
|
||||||
}
|
}</blockquote>
|
||||||
</p>
|
</div>
|
||||||
<ul>
|
</div>
|
||||||
<li><code>type</code>" "<b>{x.actor_type().unwrap_or(apb::ActorType::Person).as_ref().to_string()}</b></li>
|
<TimelineFeed tl=Timeline::new(format!("{}/outbox/page", Uri::api("users", &id.clone()))) />
|
||||||
<li><code>following</code>" "<b>{x.following().get().map(|x| x.total_items().unwrap_or(0))}</b></li>
|
}.into_view()
|
||||||
<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(),
|
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue