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;
}
#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 {
max-width: calc(100% - 2rem);
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.
use minijinja::{context, Environment, Value as JValue};
use minijinja::Environment;
use serde::Serialize;
use serde_json::{json, Value};
use std::sync::{Arc, Mutex, RwLock};
@@ -193,12 +193,7 @@ impl Ctx {
return String::new();
}
};
let ctx = context! {
lang => self.lang(),
base_uri => self.base_uri(),
me => self.jid(),
..JValue::from_serialize(&data)
};
let ctx = crate::tpl::render_context(serde_json::to_value(&data).unwrap_or_default(), &self.lang(), &self.base_uri(), &self.jid());
match tpl.render(ctx) {
Ok(s) => s.trim().to_string(),
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)
}
/// 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)]
mod tests {
use super::*;
@@ -116,4 +126,18 @@ mod tests {
let html = tpl.render(data("vi")).unwrap();
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 }})">
<ul class="list thick">
<li class="block">
<span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})">
<header class="avatar_only">
<span class="primary icon bubble color {{ me.color }}" style="background-image: url({{ me.picture }})"
onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
</span>
<div>
<a class="button color transparent active" onclick="Avatar_ajaxGetForm()" title="{{ __('avatar.change') }}">
<button class="button flat" onclick="Avatar_ajaxGetForm()">
<i class="material-symbols">person_edit</i>
</a>
</div>
</li>
</ul>
<br />
{{ __('avatar.change') }}
</button>
</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())