Pages move the way their addresses do; a record with a picture is a card; the trainer says who a business is
Test / test (pull_request) Successful in 1m13s
Test / test (pull_request) Successful in 1m13s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
748241709a
commit
309e30f36d
+89
-2
@@ -57,6 +57,8 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
// Server-side, so it can read hash.txt and link the
|
||||
// content-hashed stylesheet (hash-files = true).
|
||||
<HashedStylesheet id="leptos" options=options.clone()/>
|
||||
// Before the router, so it sees a link's click first.
|
||||
<script src="/page-motion.js" defer=true></script>
|
||||
<AutoReload options=options.clone()/>
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
@@ -1949,12 +1951,16 @@ fn ResourceValue(
|
||||
}
|
||||
if let Ok(answers) = serde_json::from_value::<Vec<Answer>>(value.clone()) {
|
||||
let mut answers = answers;
|
||||
// Cards are shown in the order they were put up, the way a
|
||||
// team or a gallery is read; a list of cases, newest first.
|
||||
let cards = transitions.is_empty() && answers.iter().all(|a| picture_of(a, bucket.as_deref()).is_some());
|
||||
answers.sort_by(|a, b| {
|
||||
let open = |s: &str| s != crate::answers::OPEN_STATE;
|
||||
open(&a.state).cmp(&open(&b.state)).then(b.submitted_ms.cmp(&a.submitted_ms))
|
||||
let when = if cards { a.submitted_ms.cmp(&b.submitted_ms) } else { b.submitted_ms.cmp(&a.submitted_ms) };
|
||||
open(&a.state).cmp(&open(&b.state)).then(when)
|
||||
});
|
||||
return view! {
|
||||
<div class="answer-list">
|
||||
<div class="answer-list" class:answer-cards=cards>
|
||||
<For
|
||||
each=move || answers.clone()
|
||||
key=|a| a.id.clone()
|
||||
@@ -2232,6 +2238,23 @@ pub fn uploaded_filename(value: &str) -> Option<&str> {
|
||||
rest.strip_prefix('-').filter(|name| shaped && !name.is_empty())
|
||||
}
|
||||
|
||||
/// Whether an uploaded file is a picture a browser shows as it is.
|
||||
pub fn is_picture(filename: &str) -> bool {
|
||||
let lower = filename.to_ascii_lowercase();
|
||||
[".jpg", ".jpeg", ".png", ".webp", ".avif", ".gif"].iter().any(|ext| lower.ends_with(ext))
|
||||
}
|
||||
|
||||
/// A record that carries a picture, in a list nobody works from, is
|
||||
/// shown rather than read: the picture, then what it says, first field
|
||||
/// as its heading. A team, a gallery of what has been built.
|
||||
fn picture_of(answer: &Answer, bucket: Option<&str>) -> Option<String> {
|
||||
let bucket = bucket?;
|
||||
answer.responses.as_object()?.iter().find_map(|(key, value)| {
|
||||
let name = uploaded_filename(value.as_str()?)?;
|
||||
is_picture(name).then(|| format!("/attachment/{bucket}/{}/{key}", answer.id))
|
||||
})
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AnswerRow(
|
||||
feature_name: String,
|
||||
@@ -2260,6 +2283,11 @@ fn AnswerRow(
|
||||
} else {
|
||||
available.is_empty()
|
||||
};
|
||||
if transitions.is_empty() {
|
||||
if let Some(picture) = picture_of(&answer, bucket.as_deref()) {
|
||||
return AnswerCard(answer, picture, link).into_any();
|
||||
}
|
||||
}
|
||||
let responses = answer
|
||||
.responses
|
||||
.as_object()
|
||||
@@ -2310,6 +2338,17 @@ fn AnswerRow(
|
||||
// A stored file shows as its name, and opens.
|
||||
if let (Some(base), Some(filename)) = (attachment_base.clone(), uploaded_filename(&text)) {
|
||||
let href = format!("{base}/{key}");
|
||||
if is_picture(filename) {
|
||||
return view! {
|
||||
<div class="answer-field">
|
||||
<span class="answer-field-key">{key}</span>
|
||||
<a class="answer-field-value" href=href.clone() rel="external">
|
||||
<img class="answer-thumb" src=href.clone() alt=filename.to_string() loading="lazy" />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
.into_any();
|
||||
}
|
||||
return view! {
|
||||
<div class="answer-field">
|
||||
<span class="answer-field-key">{key}</span>
|
||||
@@ -2412,6 +2451,36 @@ fn AnswerRow(
|
||||
</Show>
|
||||
</article>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
/// A record shown as a card: its picture, its first field as the
|
||||
/// heading and the others under it, and nothing a reader has no use
|
||||
/// for - no field names, no state, no time.
|
||||
#[allow(non_snake_case)]
|
||||
fn AnswerCard(answer: Answer, picture: String, link: Option<RowLink>) -> impl IntoView {
|
||||
let link = link.map(|l| (l.href(&answer.id, &answer.responses), l.label));
|
||||
let texts: Vec<String> = answer
|
||||
.responses
|
||||
.as_object()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|(_, v)| v.as_str())
|
||||
.filter(|v| uploaded_filename(v).is_none() && !v.trim().is_empty())
|
||||
.map(str::to_string)
|
||||
.collect();
|
||||
let heading = texts.first().cloned().unwrap_or_default();
|
||||
let rest: Vec<String> = texts.into_iter().skip(1).collect();
|
||||
view! {
|
||||
<article class="answer-row answer-card">
|
||||
<img class="answer-picture" src=picture alt=heading.clone() loading="lazy" />
|
||||
<div class="answer-card-text">
|
||||
<h4 class="answer-card-heading">{heading}</h4>
|
||||
{rest.into_iter().map(|t| view! { <p>{t}</p> }).collect_view()}
|
||||
</div>
|
||||
{link.map(|(href, label)| view! { <a class="answer-link" href=href>{label}</a> })}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
/// Iconify names are `{prefix}:{name}`, both lowercase-alphanumeric-
|
||||
@@ -3085,3 +3154,21 @@ mod responsible_tests {
|
||||
assert!(!is_responsible(None, &board));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod picture_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_record_with_a_picture_is_shown_by_it() {
|
||||
assert!(is_picture("Portrett.JPG") && is_picture("a.webp") && !is_picture("rapport.pdf"));
|
||||
let answer: Answer = serde_json::from_value(serde_json::json!({
|
||||
"id": "r1", "question_id": "kontor/ansatt", "alternative": "a", "state": "open", "submitted_ms": 1,
|
||||
"responses": { "name": "Kari", "portrett": "0f8fad5b-d9cb-469f-a165-70867728950e-kari.jpg", "cv": "0f8fad5b-d9cb-469f-a165-70867728950e-cv.pdf" }
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(picture_of(&answer, Some("ansatte")).as_deref(), Some("/attachment/ansatte/r1/portrett"));
|
||||
// Without a bucket there is nowhere to fetch it from.
|
||||
assert_eq!(picture_of(&answer, None), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user