implement code submission feature with routing and server validation
This commit is contained in:
+8
-2
@@ -2,9 +2,10 @@ use leptos::prelude::*;
|
|||||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||||
use leptos_router::{
|
use leptos_router::{
|
||||||
components::{Route, Router, Routes},
|
components::{Route, Router, Routes},
|
||||||
StaticSegment,
|
ParamSegment, StaticSegment,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::codes::{HomePage, SubmitPage};
|
||||||
use crate::manage::ManagePage;
|
use crate::manage::ManagePage;
|
||||||
|
|
||||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
@@ -36,13 +37,18 @@ pub fn App() -> impl IntoView {
|
|||||||
<Stylesheet id="leptos" href="/pkg/code_submission.css"/>
|
<Stylesheet id="leptos" href="/pkg/code_submission.css"/>
|
||||||
|
|
||||||
// sets the document title
|
// sets the document title
|
||||||
<Title text="Manage Server"/>
|
<Title text="Submit a code"/>
|
||||||
|
|
||||||
// content for this welcome page
|
// content for this welcome page
|
||||||
<Router>
|
<Router>
|
||||||
<main>
|
<main>
|
||||||
<Routes fallback=|| "Page not found.".into_view()>
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
|
<Route path=StaticSegment("") view=HomePage/>
|
||||||
<Route path=StaticSegment("manage") view=ManagePage/>
|
<Route path=StaticSegment("manage") view=ManagePage/>
|
||||||
|
<Route
|
||||||
|
path=(StaticSegment("submit"), ParamSegment("code"))
|
||||||
|
view=SubmitPage
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -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) {}
|
||||||
@@ -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,4 +1,5 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod codes;
|
||||||
pub mod manage;
|
pub mod manage;
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
|
|||||||
+8
-1
@@ -1,4 +1,5 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use leptos_router::components::A;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -6,12 +7,17 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
static SERVER_LIVE: AtomicBool = AtomicBool::new(false);
|
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")]
|
#[cfg(feature = "ssr")]
|
||||||
const STATUS_SECRET_KEY: &str = "password";
|
const STATUS_SECRET_KEY: &str = "password";
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn get_live_state() -> Result<bool, ServerFnError> {
|
async fn get_live_state() -> Result<bool, ServerFnError> {
|
||||||
Ok(SERVER_LIVE.load(Ordering::SeqCst))
|
Ok(server_live_state())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
@@ -46,6 +52,7 @@ pub fn ManagePage() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<A href="/" attr:class="home-link">"Back to home"</A>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ h1 {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.key-label {
|
.key-label {
|
||||||
color: #d3dceb;
|
color: #d3dceb;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -155,6 +160,20 @@ h1 {
|
|||||||
line-height: 1.5;
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* {
|
* {
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user