A jq-shaped resource (e.g. the GiteaStarred showcase) previously fell through to the raw <pre> JSON dump - same fallback path meant for truly unstructured data. Added a card renderer for the common case (array of plain objects): name/title as a linked heading, description as body text, everything else as small key/value chips - same visual language as AnswerRow's answer-fields, just for non-Answer data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1db558974e
commit
bd702bfbae
+96
@@ -836,11 +836,107 @@ fn ResourceValue(
|
||||
if items.is_empty() {
|
||||
return view! { <p class="resource-empty">"Nothing here yet."</p> }.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! {
|
||||
<div class="item-list">
|
||||
<For
|
||||
each=move || items.clone()
|
||||
key=|item| item.to_string()
|
||||
children=|item: serde_json::Value| view! { <ItemCard item=item/> }
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
.into_any();
|
||||
}
|
||||
}
|
||||
|
||||
view! { <pre class="resource-raw">{value.to_string()}</pre> }.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! {
|
||||
<article class="item-card">
|
||||
{(name.is_some() || url.is_some())
|
||||
.then(|| {
|
||||
view! {
|
||||
<div class="item-card-title">
|
||||
{match (name.clone(), url.clone()) {
|
||||
(Some(n), Some(u)) => {
|
||||
view! {
|
||||
<a href=u target="_blank" rel="noopener noreferrer">
|
||||
{n}
|
||||
</a>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
(Some(n), None) => view! { <span>{n}</span> }.into_any(),
|
||||
(None, Some(u)) => {
|
||||
view! {
|
||||
<a href=u.clone() target="_blank" rel="noopener noreferrer">
|
||||
{u.clone()}
|
||||
</a>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
(None, None) => ().into_any(),
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
{description.map(|d| view! { <p class="item-card-description">{d}</p> })}
|
||||
{(!extra.is_empty())
|
||||
.then(|| {
|
||||
view! {
|
||||
<div class="item-card-fields">
|
||||
<For
|
||||
each=move || extra.clone()
|
||||
key=|(k, _)| k.clone()
|
||||
children=|(k, v)| {
|
||||
view! {
|
||||
<span class="item-card-field">
|
||||
<span class="item-card-field-key">{k}</span>
|
||||
{v}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn AnswerRow(
|
||||
question_id: String,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user