feat(web): add footer

This commit is contained in:
Kristofers Solo 2025-11-26 17:33:10 +02:00
parent a93ff3f920
commit 898d5f7195
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
5 changed files with 144 additions and 74 deletions

View File

@ -1,11 +1,10 @@
use crate::pages::{aes::AesPage, des::DesPage, home::Home}; use crate::pages::{aes::AesPage, des::DesPage, footer::Footer, header::Header, home::Home};
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_router::{ use leptos_router::{
StaticSegment, StaticSegment,
components::{A, Route, Router, Routes}, components::{Route, Router, Routes},
}; };
use std::fmt::Display;
#[must_use] #[must_use]
pub fn shell(options: LeptosOptions) -> impl IntoView { pub fn shell(options: LeptosOptions) -> impl IntoView {
@ -26,60 +25,15 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
} }
} }
#[derive(Clone, Copy, PartialEq)]
enum Theme {
Light,
Dark,
}
impl Theme {
const fn inverse(self) -> Self {
match self {
Self::Light => Self::Dark,
Self::Dark => Self::Light,
}
}
}
impl Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Light => "☀️ Light",
Self::Dark => "🌙 Dark",
};
f.write_str(s)
}
}
#[component] #[component]
// Provides context that manages stylesheets, titles, meta tags, etc. // Provides context that manages stylesheets, titles, meta tags, etc.
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
provide_meta_context(); provide_meta_context();
let (theme, set_theme) = signal(Theme::Dark);
let toggle_theme = move |_| {
set_theme.update(|t| *t = t.inverse());
if let Some(body) = document().body() {
let class_list = body.class_list();
match theme.get() {
Theme::Light => {
let _ = class_list.remove_1("dark-theme");
let _ = class_list.add_1("light-theme");
}
Theme::Dark => {
let _ = class_list.remove_1("light-theme");
let _ = class_list.add_1("dark-theme");
}
}
}
};
view! { view! {
// injects a stylesheet into the document <head> // injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet // id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/web2.css" /> <Stylesheet id="leptos" href="/pkg/web.css" />
// sets the document title // sets the document title
<Title text="Cipher Workshop" /> <Title text="Cipher Workshop" />
@ -87,22 +41,7 @@ pub fn App() -> impl IntoView {
// content for this welcome page // content for this welcome page
<Router> <Router>
<div class="app-containter"> <div class="app-containter">
<nav class="main-nav"> <Header />
<ul>
<li>
<A href="/">"Home"</A>
</li>
<li>
<A href="/des">"DES"</A>
</li>
<li>
<A href="/aes">"AES"</A>
</li>
</ul>
<button class="theme-toggle" on:click=toggle_theme>
{move || theme.get().to_string()}
</button>
</nav>
<main> <main>
<Routes fallback=|| "Page not found.".into_view()> <Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("/") view=Home /> <Route path=StaticSegment("/") view=Home />
@ -110,6 +49,7 @@ pub fn App() -> impl IntoView {
<Route path=StaticSegment("/aes") view=AesPage /> <Route path=StaticSegment("/aes") view=AesPage />
</Routes> </Routes>
</main> </main>
<Footer />
</div> </div>
</Router> </Router>
} }

23
web/src/pages/footer.rs Normal file
View File

@ -0,0 +1,23 @@
use leptos::prelude::*;
use leptos_router::components::A;
#[component]
pub fn Footer() -> impl IntoView {
view! {
<footer class="app-footer">
<div class="footer-content">
<p>
"🔒 " <strong>"Client-Side Security:"</strong>
" All encryption and decryption operations happen entirely in your browser. "
"No data is ever sent to a server. "
"You can verify this by disconnecting your internet."
</p>
<div class="footer-links">
<A href="https://github.com/kristoferssolo/cipher-workshop" target="_blank">
"View Source on GitHub"
</A>
</div>
</div>
</footer>
}
}

70
web/src/pages/header.rs Normal file
View File

@ -0,0 +1,70 @@
use leptos::prelude::*;
use leptos_router::components::A;
use std::fmt::Display;
#[component]
pub fn Header() -> impl IntoView {
let (theme, set_theme) = signal(Theme::Dark);
let toggle_theme = move |_| {
set_theme.update(|t| *t = t.inverse());
if let Some(body) = document().body() {
let class_list = body.class_list();
match theme.get() {
Theme::Light => {
let _ = class_list.remove_1("dark-theme");
let _ = class_list.add_1("light-theme");
}
Theme::Dark => {
let _ = class_list.remove_1("light-theme");
let _ = class_list.add_1("dark-theme");
}
}
}
};
view! {
<nav class="main-nav">
<ul>
<li>
<A href="/">"Home"</A>
</li>
<li>
<A href="/des">"DES"</A>
</li>
<li>
<A href="/aes">"AES"</A>
</li>
</ul>
<button class="theme-toggle" on:click=toggle_theme>
{move || theme.get().to_string()}
</button>
</nav>
}
}
#[derive(Clone, Copy, PartialEq)]
enum Theme {
Light,
Dark,
}
impl Theme {
const fn inverse(self) -> Self {
match self {
Self::Light => Self::Dark,
Self::Dark => Self::Light,
}
}
}
impl Display for Theme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Light => "☀️ Light",
Self::Dark => "🌙 Dark",
};
f.write_str(s)
}
}

View File

@ -1,3 +1,5 @@
pub mod aes; pub mod aes;
pub mod des; pub mod des;
pub mod footer;
pub mod header;
pub mod home; pub mod home;

View File

@ -27,6 +27,7 @@ $l-hl-low: #f4ede8;
$l-hl-high: #cecacd; $l-hl-high: #cecacd;
$control-height: 46px; $control-height: 46px;
$trans-speed: 0.3s ease;
:root, :root,
body.dark-theme { body.dark-theme {
@ -66,20 +67,29 @@ body.light-theme {
--focus-ring: #{rgba($l-rose, 0.3)}; --focus-ring: #{rgba($l-rose, 0.3)};
} }
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
body { body {
font-family: "Inter", "Segoe UI", sans-serif; font-family: "Inter", "Segoe UI", sans-serif;
background-color: var(--bg-body); background-color: var(--bg-body);
color: var(--text-main); color: var(--text-main);
margin: 0; margin: 0;
transition: transition:
background-color 0.3s, background-color $trans-speed,
color 0.3s; color $trans-speed;
} }
.app-containter { .app-containter {
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
min-height: 100vh;
} }
.main-nav { .main-nav {
@ -89,6 +99,9 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
transition:
background-color $trans-speed,
border-color $trans-speed;
ul { ul {
list-style: none; list-style: none;
@ -111,6 +124,13 @@ body {
} }
} }
main {
flex: 1;
padding: 2rem;
width: 100%;
box-sizing: border-box;
}
.theme-toggle { .theme-toggle {
background: transparent; background: transparent;
border: 1px solid var(--border); border: 1px solid var(--border);
@ -135,11 +155,13 @@ body {
text-align: center; text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--text-muted); color: var(--text-muted);
transition:
background-color $trans-speed,
border-color $trans-speed,
color $trans-speed;
} }
.footer-content { .footer-content {
max-width: 800px;
margin: 0 auto;
padding: 0 1rem; padding: 0 1rem;
p { p {
@ -167,16 +189,16 @@ body {
} }
} }
main {
padding: 2rem;
}
.cipher-card { .cipher-card {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 2rem; padding: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
transition:
background-color $trans-speed,
border-color $trans-speed,
box-shadow $trans-speed;
} }
.card-header { .card-header {
@ -400,6 +422,19 @@ main {
} }
} }
.form-group input[type="text"],
.form-group textarea,
.format-controls,
.radio-group,
.result-box,
.result-toolbar,
.format-controls select {
transition:
background-color $trans-speed,
border-color $trans-speed,
color $trans-speed;
}
.btn-copy { .btn-copy {
background: transparent; background: transparent;
border: none; border: none;