use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
hooks::{use_location, use_navigate, use_query_map},
path, NavigateOptions,
};
use serde::{Deserialize, Serialize};
use crate::answers::{Answer, TransitionAnswer};
use crate::auth::{current_user, User};
use crate::content::{is_qualified, Alternative, Question, Transition};
use crate::resource::get_resource;
/// The visible site name/wordmark - "portal" is just this codebase's
/// working title, not necessarily what any given deployment is called.
/// Baked in at *build* time (`option_env!`, not `std::env::var`) so the
/// ssr and hydrate targets - compiled separately - end up with the
/// identical value without a runtime round-trip or context-passing
/// between them; set `SITE_NAME` for both `cargo leptos` invocations
/// (ssr and hydrate) to change it, e.g. `SITE_NAME=uhhm`. CSS uppercases
/// the wordmark for display regardless of the source casing here.
const SITE_NAME: &str = match option_env!("SITE_NAME") {
Some(name) => name,
None => "portal",
};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
}>
}
}
#[component]
fn QuestionPage() -> impl IntoView {
let location = use_location();
let query = use_query_map();
let path = Memo::new(move |_| {
let p = location.pathname.get();
if p.is_empty() {
"/".to_string()
} else {
p
}
});
let parent_hash = Memo::new(move |_| query.with(|q| q.get("chain")));
let question = Resource::new(move || path.get(), get_question);
let user = Resource::new(|| (), |_| current_user());
view! {
"loading…"
}
}>
{move || {
// Both read unconditionally (not nested inside the match
// below) so Suspense tracks both from the first pass,
// rather than only discovering `user` once `question`
// has already resolved.
let user_res = user.get();
let question_res = question.get();
question_res
.map(|res| match res {
Ok(Some(q)) => {
let current = user_res.and_then(|r| r.ok()).flatten();
view! {
}
.into_any()
}
_ => view! { }.into_any(),
})
}}
}
}
#[component]
fn QuestionView(
question: Question,
parent_hash: Option,
user: Option,
) -> impl IntoView {
let question_id = question.id.clone();
// Generic: any question with `qualifies` set renders this same gate
// instead of its alternatives - "/review" isn't a special case, it's
// just a question that happens to have `qualifies` set.
if !is_qualified(user.as_ref(), &question) {
let signed_in = user.is_some();
return view! {
{if signed_in {
view! {
"This part of the site is for organizational owners — sign in with that account to take a look."
}
.into_any()
}
// Binds the actual "YES - Rasterized Lines" piece live at uhhm.no
// (ported to `public/yes.js`, an ES module now instead of a page-owning
// script) as a typed JS class - the idiomatic wasm-bindgen way to drive
// an existing JS module from Rust, rather than re-implementing canvas
// animation logic here.
#[cfg(feature = "hydrate")]
mod yes {
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/yes.js")]
extern "C" {
#[wasm_bindgen(js_name = RasterizedYES)]
pub type RasterizedYes;
#[wasm_bindgen(constructor, js_class = "RasterizedYES")]
pub fn new() -> RasterizedYes;
#[wasm_bindgen(method)]
pub fn stop(this: &RasterizedYes);
}
}
#[component]
fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView {
// Only the landing page gets the full interactive piece - it's the
// one page this is actually "the" hero for; other pages (thank-you
// pages, /review) get the plain dark header below. Starts the
// animation once the canvas elements exist (post-mount, via
// Effect), and explicitly stops the requestAnimationFrame loop on
// unmount - the original page-owning script never needed this since
// navigating away meant a full document unload, which doesn't
// happen in an SPA.
#[cfg(feature = "hydrate")]
if show_yes {
// `on_cleanup` requires Send + Sync (even single-threaded, wasm),
// which a JS-backed value never is - `StoredValue`'s `LocalStorage`
// variant is the established way around this (same pattern
// cnats' WebRTC call state uses for its own non-Send peer map).
let instance: StoredValue