diff --git a/src/app.rs b/src/app.rs index 3b80b04..d3be29f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -836,11 +836,107 @@ fn ResourceValue( if items.is_empty() { return view! {

"Nothing here yet."

}.into_any(); } + // A list of plain objects (e.g. a jq-shaped GiteaStarred/Url + // resource) - render as cards rather than dumping raw JSON. + // Generic on purpose, no content-declared "kind": `name`/`title` + // becomes the heading (linked, if `url`/`html_url` is present), + // `description` becomes the body text, and every other non-null + // field becomes a small key/value chip - same idea as + // `AnswerRow`'s `answer-fields`, just for data that isn't an + // `Answer`. + if items.iter().all(|i| i.is_object()) { + let items = items.clone(); + return view! { +
+ } + /> +
+ } + .into_any(); + } } view! {
{value.to_string()}
}.into_any() } +/// One card in a generic (non-`Answer`) resource list - see +/// `ResourceValue`. +#[component] +fn ItemCard(item: serde_json::Value) -> impl IntoView { + let obj = item.as_object().cloned().unwrap_or_default(); + let text_field = |keys: &[&str]| { + keys.iter().find_map(|k| obj.get(*k)).and_then(|v| v.as_str()).map(str::to_string) + }; + let name = text_field(&["name", "title"]); + let description = text_field(&["description"]); + let url = text_field(&["url", "html_url"]); + + let known = ["name", "title", "description", "url", "html_url"]; + let extra: Vec<(String, String)> = obj + .iter() + .filter(|(k, v)| !known.contains(&k.as_str()) && !v.is_null()) + .map(|(k, v)| { + let text = v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()); + (k.clone(), text) + }) + .collect(); + + view! { +
+ {(name.is_some() || url.is_some()) + .then(|| { + view! { +
+ {match (name.clone(), url.clone()) { + (Some(n), Some(u)) => { + view! { + + {n} + + } + .into_any() + } + (Some(n), None) => view! { {n} }.into_any(), + (None, Some(u)) => { + view! { + + {u.clone()} + + } + .into_any() + } + (None, None) => ().into_any(), + }} +
+ } + })} + {description.map(|d| view! {

{d}

})} + {(!extra.is_empty()) + .then(|| { + view! { +
+ + {k} + {v} + + } + } + /> +
+ } + })} +
+ } +} + #[component] fn AnswerRow( question_id: String, diff --git a/style/main.css b/style/main.css index 7f9c929..6d39aee 100644 --- a/style/main.css +++ b/style/main.css @@ -374,6 +374,43 @@ input:focus, textarea:focus { .answer-action:hover { border-color: var(--accent); color: var(--accent); } .answer-action:disabled { opacity: 0.5; cursor: wait; } +.item-list { display: grid; gap: 0.8rem; } + +.item-card { + border: 1px solid var(--line); + border-radius: 0.8rem; + padding: 1rem 1.1rem; + display: grid; + gap: 0.4rem; +} + +.item-card-title { + font-family: var(--sans); + font-weight: 600; + font-size: 1rem; +} + +.item-card-title a { color: var(--ink); text-decoration: none; } +.item-card-title a:hover { color: var(--accent); text-decoration: underline; } + +.item-card-description { font-size: 0.9rem; color: var(--ink-dim); } + +.item-card-fields { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 0.9rem; + margin-top: 0.2rem; +} + +.item-card-field { font-size: 0.82rem; color: var(--ink-dim); } + +.item-card-field-key { + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 0.68rem; + margin-right: 0.3rem; +} + @media (max-width: 640px) { .hero { padding: 3rem 1.25rem 2rem; } .alt-card { padding: 1.4rem 1.3rem; }