fix profile page

This commit is contained in:
nnduc
2026-10-24 23:27:39 +07:00
parent 6946109ae2
commit 9f44778f21
5 changed files with 113 additions and 21 deletions
+29
View File
@@ -9,7 +9,36 @@
font-size: 5rem; font-size: 5rem;
} }
#avatar header.avatar_only {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 2rem 1rem 1rem;
}
#avatar header.avatar_only span.primary {
margin: 0;
cursor: pointer;
background-size: cover;
background-position: center;
}
form[name=bannerform] img.avatar { form[name=bannerform] img.avatar {
max-width: calc(100% - 2rem); max-width: calc(100% - 2rem);
margin: 1rem; margin: 1rem;
} }
#avatar header.avatar_only {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 2rem 1rem 1rem;
}
#avatar header.avatar_only span.primary {
margin: 0;
cursor: pointer;
background-size: cover;
background-position: center;
}
+2 -7
View File
@@ -1,6 +1,6 @@
//! Shared application state and the per-call context given to the widgets. //! Shared application state and the per-call context given to the widgets.
use minijinja::{context, Environment, Value as JValue}; use minijinja::Environment;
use serde::Serialize; use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
@@ -193,12 +193,7 @@ impl Ctx {
return String::new(); return String::new();
} }
}; };
let ctx = context! { let ctx = crate::tpl::render_context(serde_json::to_value(&data).unwrap_or_default(), &self.lang(), &self.base_uri(), &self.jid());
lang => self.lang(),
base_uri => self.base_uri(),
me => self.jid(),
..JValue::from_serialize(&data)
};
match tpl.render(ctx) { match tpl.render(ctx) {
Ok(s) => s.trim().to_string(), Ok(s) => s.trim().to_string(),
Err(e) => { Err(e) => {
+24
View File
@@ -91,6 +91,16 @@ fn add_emojis(state: &State, s: String, no_title: Option<bool>) -> Value {
Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html) Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html)
} }
/// The widget data wins over the defaults (the Avatar templates pass their own `me`)
pub fn render_context(mut data: serde_json::Value, lang: &str, base_uri: &str, me: &str) -> Value {
if let Some(map) = data.as_object_mut() {
for (k, v) in [("lang", lang), ("base_uri", base_uri), ("me", me)] {
map.entry(k).or_insert_with(|| v.into());
}
}
Value::from_serialize(&data)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -116,4 +126,18 @@ mod tests {
let html = tpl.render(data("vi")).unwrap(); let html = tpl.render(data("vi")).unwrap();
assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ")); assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ"));
} }
#[test]
fn profile_picture_header() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
i18n::init(&root.join("frontend"));
let env = environment(&root.join("templates"));
let tpl = env.get_template("Avatar/_avatar.html").unwrap();
let data = json!({ "me": { "picture": "/avatar/abc", "color": "yellow" } });
let html = tpl.render(render_context(data, "en", "/", "alice@example.org")).unwrap();
assert!(html.contains("url(/avatar/abc)") && html.contains("color yellow"), "{html}");
// The defaults are still there
let html = env.render_str("{{ me }} {{ lang }}", render_context(json!({}), "vi", "/", "alice@example.org")).unwrap();
assert_eq!(html, "alice@example.org vi");
}
} }
+6 -11
View File
@@ -1,14 +1,9 @@
<header class="big top color {{ me.color }}" style="background-image: url({{ me.picture }})"> <header class="avatar_only">
<ul class="list thick"> <span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})"
<li class="block"> onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
<span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})">
</span> </span>
<div> <button class="button flat" onclick="Avatar_ajaxGetForm()">
<a class="button color transparent active" onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
<i class="material-symbols">person_edit</i> <i class="material-symbols">person_edit</i>
</a> {{ __('avatar.change') }}
</div> </button>
</li>
</ul>
<br />
</header> </header>
+49
View File
@@ -0,0 +1,49 @@
"""Set a profile picture and check it on the profile (configuration) page."""
import asyncio, os
from playwright.async_api import async_playwright
from browser import login, BASE, OUT
S = os.path.dirname(os.path.abspath(__file__))
async def header(page):
return await page.evaluate("""() => {
const h = document.querySelector('#avatar header');
const s = document.querySelector('#avatar header span.primary');
return h ? [getComputedStyle(h).backgroundImage, s && getComputedStyle(s).backgroundImage,
h.getBoundingClientRect().height] : null;
}""")
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "avatar")
bad = []
page.on("response", lambda r: r.status >= 400 and bad.append(f"{r.status} {r.url}"))
await page.goto(BASE + "/configuration")
await page.wait_for_timeout(4000)
print("before:", await header(page))
await page.screenshot(path=f"{OUT}/avatar_1_before.png")
await page.evaluate("Avatar_ajaxGetForm()")
await page.wait_for_selector("#dialog input[type=file]", state="attached", timeout=10000)
await page.set_input_files("#dialog input[type=file]", f"{S}/test.png")
await page.wait_for_timeout(2000)
print("photobin length:", await page.evaluate("document.querySelector('[name=photobin]').value.length"))
await page.screenshot(path=f"{OUT}/avatar_2_form.png")
await page.click("#dialog footer button:last-child")
await page.wait_for_timeout(4000)
print("after submit:", await header(page))
await page.screenshot(path=f"{OUT}/avatar_3_after.png")
await page.goto(BASE + "/configuration")
await page.wait_for_timeout(4000)
print("after reload:", await header(page))
print("nav avatar:", await page.evaluate("[...document.querySelectorAll('nav img, nav span.primary')].map(e => e.src || getComputedStyle(e).backgroundImage).slice(0, 3)"))
await page.screenshot(path=f"{OUT}/avatar_4_reload.png")
for e in errors + bad:
print(e)
await browser.close()
asyncio.run(main())