- QuestionNav: every other question the visitor currently qualifies for, rendered under each page's alternatives - a concern reaches the people it speaks to without claiming front-page space, and an owner sees the gated desks in the same nav. Context-dependent by session (list_qualifying_questions filters via is_qualified). - An alternative with an action but nothing to submit, confirm, or record is a gateway: its button now renders as a real link instead of a form submit that did nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4d71b6ccd0
commit
e237a4b3c0
+107
-22
@@ -196,19 +196,23 @@ fn QuestionView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
key=|a| a.name.clone()
|
key=|a| a.name.clone()
|
||||||
children=move |alt: Alternative| {
|
children={
|
||||||
view! {
|
let question_id = question_id.clone();
|
||||||
<AlternativeCard
|
move |alt: Alternative| {
|
||||||
question_id=question_id.clone()
|
view! {
|
||||||
alternative=alt
|
<AlternativeCard
|
||||||
parent_hash=parent_hash.clone()
|
question_id=question_id.clone()
|
||||||
query_email=query_email.clone()
|
alternative=alt
|
||||||
/>
|
parent_hash=parent_hash.clone()
|
||||||
|
query_email=query_email.clone()
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<QuestionNav current_id=question_id/>
|
||||||
{question
|
{question
|
||||||
.responsible
|
.responsible
|
||||||
.clone()
|
.clone()
|
||||||
@@ -219,6 +223,39 @@ fn QuestionView(
|
|||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every other question the visitor currently qualifies for, as a
|
||||||
|
/// small nav - how a concern reaches the people it speaks to without
|
||||||
|
/// claiming front-page space (an owner also sees the gated desks here).
|
||||||
|
#[component]
|
||||||
|
fn QuestionNav(current_id: String) -> impl IntoView {
|
||||||
|
let nav = Resource::new(|| (), |_| list_qualifying_questions());
|
||||||
|
view! {
|
||||||
|
<Suspense fallback=|| ()>
|
||||||
|
{move || {
|
||||||
|
let current_id = current_id.clone();
|
||||||
|
nav.get().and_then(|res| res.ok()).map(|items| {
|
||||||
|
let others: Vec<(String, String)> = items
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(id, _)| *id != current_id)
|
||||||
|
.collect();
|
||||||
|
(!others.is_empty()).then(|| view! {
|
||||||
|
<nav class="question-nav">
|
||||||
|
<span class="question-nav-lead">"Also worth asking"</span>
|
||||||
|
<For
|
||||||
|
each=move || others.clone()
|
||||||
|
key=|(id, _)| id.clone()
|
||||||
|
children=|(id, name): (String, String)| {
|
||||||
|
view! { <a href=id>{name}</a> }.into_any()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// "Asked by X — contact them if you get stuck."
|
/// "Asked by X — contact them if you get stuck."
|
||||||
/// The mailto address is assembled from `data-user`/`data-domain` on a
|
/// The mailto address is assembled from `data-user`/`data-domain` on a
|
||||||
/// real mouse event, never baked into the server-rendered `href` -
|
/// real mouse event, never baked into the server-rendered `href` -
|
||||||
@@ -463,6 +500,16 @@ fn AlternativeCard(
|
|||||||
|
|
||||||
let has_action = alternative.action.is_some();
|
let has_action = alternative.action.is_some();
|
||||||
let has_requirements = alternative.features.iter().any(|f| !f.requirements.is_empty());
|
let has_requirements = alternative.features.iter().any(|f| !f.requirements.is_empty());
|
||||||
|
// Nothing to submit, nothing to confirm, nothing to record: the
|
||||||
|
// alternative is a gateway to another question, and its button is
|
||||||
|
// just a link (a form submit would be a click that does nothing).
|
||||||
|
let is_gateway = has_action
|
||||||
|
&& !has_requirements
|
||||||
|
&& alternative.record_as.is_none()
|
||||||
|
&& alternative
|
||||||
|
.features
|
||||||
|
.iter()
|
||||||
|
.all(|f| f.resource.as_ref().is_none_or(|r| r.transitions.is_empty()));
|
||||||
|
|
||||||
// Shared across every resource feature on this alternative: which
|
// Shared across every resource feature on this alternative: which
|
||||||
// row(s) have a transition selected but not yet confirmed, keyed by
|
// row(s) have a transition selected but not yet confirmed, keyed by
|
||||||
@@ -841,19 +888,30 @@ fn AlternativeCard(
|
|||||||
children=move |e| view! { <p class="alt-encouragement">{e}</p> }
|
children=move |e| view! { <p class="alt-encouragement">{e}</p> }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form on:submit=on_submit>
|
{if is_gateway {
|
||||||
<button
|
let href = alternative.action.clone().unwrap_or_default();
|
||||||
type="submit"
|
view! {
|
||||||
class="alt-submit"
|
<a class="alt-submit" href=href>{button_label.clone()}</a>
|
||||||
disabled=move || {
|
}
|
||||||
(!has_requirements && pending_transitions.get().is_empty())
|
.into_any()
|
||||||
|| submit.pending().get()
|
} else {
|
||||||
|| transition_batch.pending().get()
|
view! {
|
||||||
}
|
<form on:submit=on_submit>
|
||||||
>
|
<button
|
||||||
{button_label.clone()}
|
type="submit"
|
||||||
</button>
|
class="alt-submit"
|
||||||
</form>
|
disabled=move || {
|
||||||
|
(!has_requirements && pending_transitions.get().is_empty())
|
||||||
|
|| submit.pending().get()
|
||||||
|
|| transition_batch.pending().get()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{button_label.clone()}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -1372,6 +1430,33 @@ pub async fn get_question(path: String) -> Result<Option<Question>, ServerFnErro
|
|||||||
Ok(state.questions.load().get(&path).cloned())
|
Ok(state.questions.load().get(&path).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every question the current visitor qualifies for, as (id, name) -
|
||||||
|
/// the site nav's data. Context-dependent: an owner session sees the
|
||||||
|
/// gated pages too, an anonymous visitor only the public ones.
|
||||||
|
#[server]
|
||||||
|
pub async fn list_qualifying_questions() -> Result<Vec<(String, String)>, ServerFnError> {
|
||||||
|
use crate::auth::{User, SESSION_USER_KEY};
|
||||||
|
use crate::content::is_qualified;
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
let state = expect_context::<AppState>();
|
||||||
|
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||||||
|
let user = session
|
||||||
|
.get::<User>(SESSION_USER_KEY)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut out: Vec<(String, String)> = state
|
||||||
|
.questions
|
||||||
|
.load()
|
||||||
|
.values()
|
||||||
|
.filter(|q| is_qualified(user.as_ref(), q))
|
||||||
|
.map(|q| (q.id.clone(), q.name.clone()))
|
||||||
|
.collect();
|
||||||
|
out.sort();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct SubmitResult {
|
pub struct SubmitResult {
|
||||||
pub next: Option<String>,
|
pub next: Option<String>,
|
||||||
|
|||||||
@@ -239,6 +239,36 @@ main.not-found {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.question-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5rem 1.2rem;
|
||||||
|
margin: 2.2rem auto 0;
|
||||||
|
max-width: 44rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav-lead {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav a {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.question-responsible {
|
.question-responsible {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--ink-dim);
|
color: var(--ink-dim);
|
||||||
|
|||||||
Reference in New Issue
Block a user