feat(web client): content pages

This commit is contained in:
Henri Bourcereau 2026-05-25 16:14:25 +02:00
parent 58f5722551
commit 6fd3499d7b
15 changed files with 312 additions and 11 deletions

View file

@ -64,6 +64,12 @@ pub struct GameDetail {
pub participants: Vec<Participant>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PageContent {
pub title: String,
pub content: String,
}
// ── Request bodies ────────────────────────────────────────────────────────────
#[derive(Serialize)]
@ -242,6 +248,18 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
}
}
pub async fn get_page(slug: &str, lang: &str) -> Result<PageContent, String> {
let resp = gloo_net::http::Request::get(&url(&format!("/pages/{slug}?lang={lang}")))
.send()
.await
.map_err(|e| e.to_string())?;
if resp.status() == 200 {
resp.json::<PageContent>().await.map_err(|e| e.to_string())
} else {
Err(format!("status {}", resp.status()))
}
}
// ── Utilities ─────────────────────────────────────────────────────────────────
/// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`.

View file

@ -21,9 +21,9 @@ use crate::game::trictrac::backend::TrictracBackend;
use crate::game::trictrac::types::{GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState};
use crate::i18n::*;
use crate::portal::{
account::AccountPage, forgot_password::ForgotPasswordPage, game_detail::GameDetailPage,
lobby::LobbyPage, profile::ProfilePage, reset_password::ResetPasswordPage,
verify_email::VerifyEmailPage,
account::AccountPage, content_page::ContentPage, forgot_password::ForgotPasswordPage,
game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage,
reset_password::ResetPasswordPage, verify_email::VerifyEmailPage,
};
use trictrac_store::CheckerMove;
@ -432,6 +432,7 @@ pub fn App() -> impl IntoView {
<Route path=path!("/verify-email") view=VerifyEmailPage />
<Route path=path!("/forgot-password") view=ForgotPasswordPage />
<Route path=path!("/reset-password") view=ResetPasswordPage />
<Route path=path!("/page/:slug") view=ContentPage />
</Routes>
</main>

View file

@ -0,0 +1,51 @@
use leptos::prelude::*;
use leptos_router::hooks::use_params_map;
use pulldown_cmark::{Options, Parser, html};
use crate::api;
use crate::i18n::*;
#[component]
pub fn ContentPage() -> impl IntoView {
let params = use_params_map();
let slug = move || params.read().get("slug").unwrap_or_default();
let i18n = use_i18n();
let page = LocalResource::new(move || {
let s = slug();
let lang = match i18n.get_locale() {
Locale::en => "en",
Locale::fr => "fr",
};
async move { api::get_page(&s, lang).await }
});
view! {
<div class="portal-main">
{move || match page.get().map(|sw| sw.take()) {
None => view! {
<p class="portal-loading">{t!(i18n, loading)}</p>
}.into_any(),
Some(Err(_)) => view! {
<p class="portal-empty">"Page not found."</p>
}.into_any(),
Some(Ok(p)) => {
let html = md_to_html(&p.content);
view! {
<div class="portal-card content-page" inner_html=html />
}.into_any()
}
}}
</div>
}
}
fn md_to_html(md: &str) -> String {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
let parser = Parser::new_ext(md, opts);
let mut output = String::new();
html::push_html(&mut output, parser);
output
}

View file

@ -1,4 +1,5 @@
pub mod account;
pub mod content_page;
pub mod forgot_password;
pub mod game_detail;
pub mod lobby;