implement code submission feature with routing and server validation

This commit is contained in:
2026-10-06 14:33:29 -05:00
parent 5509a6d222
commit acf23d5430
6 changed files with 146 additions and 3 deletions
+8 -2
View File
@@ -2,9 +2,10 @@ use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
ParamSegment, StaticSegment,
};
use crate::codes::{HomePage, SubmitPage};
use crate::manage::ManagePage;
pub fn shell(options: LeptosOptions) -> impl IntoView {
@@ -36,13 +37,18 @@ pub fn App() -> impl IntoView {
<Stylesheet id="leptos" href="/pkg/code_submission.css"/>
// sets the document title
<Title text="Manage Server"/>
<Title text="Submit a code"/>
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/>
<Route path=StaticSegment("manage") view=ManagePage/>
<Route
path=(StaticSegment("submit"), ParamSegment("code"))
view=SubmitPage
/>
</Routes>
</main>
</Router>
+5
View File
@@ -0,0 +1,5 @@
pub const CODE: &str = "Qj4eAn";
pub const LIVE_MESSAGE: &str = "Welcome! Your code has been accepted.";
pub const NOT_LIVE_MESSAGE: &str = "Thanks for submitting your code. Please try again later.";
pub(super) fn on_submit(_live: bool) {}
+105
View File
@@ -0,0 +1,105 @@
use leptos::prelude::*;
use leptos_router::{
components::A,
hooks::{use_navigate, use_params_map},
NavigateOptions,
};
#[cfg(feature = "ssr")]
mod code1;
#[server]
async fn submit_code(code: String) -> Result<String, ServerFnError> {
let live = crate::manage::server_live_state();
let message = if code == crate::codes::code1::CODE {
crate::codes::code1::on_submit(live);
if live {
crate::codes::code1::LIVE_MESSAGE
} else {
crate::codes::code1::NOT_LIVE_MESSAGE
}
} else {
"This code is not recognized."
};
Ok(message.to_string())
}
fn encode_path_segment(value: &str) -> String {
value
.bytes()
.map(|byte| {
if byte.is_ascii_alphanumeric() || b"-_.~".contains(&byte) {
(byte as char).to_string()
} else {
format!("%{byte:02X}")
}
})
.collect()
}
#[component]
pub fn HomePage() -> impl IntoView {
let (code, set_code) = signal(String::new());
let navigate = use_navigate();
let on_submit = move |event: leptos::ev::SubmitEvent| {
event.prevent_default();
let code = encode_path_segment(code.get_untracked().trim());
if !code.is_empty() {
navigate(&format!("/submit/{code}"), NavigateOptions::default());
}
};
view! {
<section class="status-card">
<p class="eyebrow">"CODE SUBMISSION"</p>
<h1>"Submit a code"</h1>
<p class="intro">"Enter your code to continue."</p>
<form class="submit-form" on:submit=on_submit>
<label class="key-label" for="submission-code">"Code"</label>
<input
id="submission-code"
class="key-input"
type="text"
placeholder="Enter your code"
autocomplete="off"
required
prop:value=code
on:input=move |event| set_code.set(event_target_value(&event))
/>
<button class="status-button go-live" type="submit">"Submit"</button>
</form>
</section>
}
}
#[component]
pub fn SubmitPage() -> impl IntoView {
let params = use_params_map();
let submitted_code = move || params.with(|params| params.get("code").unwrap_or_default());
let submission = Resource::new(move || submitted_code(), |code| submit_code(code));
view! {
<section class="status-card">
<p class="eyebrow">"CODE SUBMISSION"</p>
<Suspense fallback=|| view! { <p class="loading">"Checking code..."</p> }>
{move || Suspend::new(async move {
match submission.await {
Ok(message) => view! {
<h1>"Submission result"</h1>
<p class="intro">{message}</p>
}.into_any(),
Err(error) => view! {
<p class="error-message" role="alert">
"Unable to check code: " {error.to_string()}
</p>
}.into_any(),
}
})}
</Suspense>
<A href="/" attr:class="home-link">"Back to home"</A>
</section>
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod app;
pub mod codes;
pub mod manage;
#[cfg(feature = "hydrate")]
+8 -1
View File
@@ -1,4 +1,5 @@
use leptos::prelude::*;
use leptos_router::components::A;
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicBool, Ordering};
@@ -6,12 +7,17 @@ use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(feature = "ssr")]
static SERVER_LIVE: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "ssr")]
pub(crate) fn server_live_state() -> bool {
SERVER_LIVE.load(Ordering::SeqCst)
}
#[cfg(feature = "ssr")]
const STATUS_SECRET_KEY: &str = "password";
#[server]
async fn get_live_state() -> Result<bool, ServerFnError> {
Ok(SERVER_LIVE.load(Ordering::SeqCst))
Ok(server_live_state())
}
#[server]
@@ -46,6 +52,7 @@ pub fn ManagePage() -> impl IntoView {
}
})}
</Suspense>
<A href="/" attr:class="home-link">"Back to home"</A>
</section>
}
}
+19
View File
@@ -50,6 +50,11 @@ h1 {
gap: 12px;
}
.submit-form {
display: grid;
gap: 12px;
}
.key-label {
color: #d3dceb;
font-size: 0.9rem;
@@ -155,6 +160,20 @@ h1 {
line-height: 1.5;
}
.home-link {
display: inline-block;
margin-top: 24px;
color: #8bbaff;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
}
.home-link:hover {
color: #b3d2ff;
text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;