From 898d5f71958b65b8488c1b2da35199c2f2175ecd Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 26 Nov 2025 17:33:10 +0200 Subject: [PATCH] feat(web): add footer --- web/src/app.rs | 70 +++-------------------------------------- web/src/pages/footer.rs | 23 ++++++++++++++ web/src/pages/header.rs | 70 +++++++++++++++++++++++++++++++++++++++++ web/src/pages/mod.rs | 2 ++ web/style/main.scss | 53 +++++++++++++++++++++++++------ 5 files changed, 144 insertions(+), 74 deletions(-) create mode 100644 web/src/pages/footer.rs create mode 100644 web/src/pages/header.rs diff --git a/web/src/app.rs b/web/src/app.rs index 662ef70..92592c5 100644 --- a/web/src/app.rs +++ b/web/src/app.rs @@ -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_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_router::{ StaticSegment, - components::{A, Route, Router, Routes}, + components::{Route, Router, Routes}, }; -use std::fmt::Display; #[must_use] 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] // Provides context that manages stylesheets, titles, meta tags, etc. pub fn App() -> impl IntoView { 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! { // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet - + // sets the document title @@ -87,22 +41,7 @@ pub fn App() -> impl IntoView { // content for this welcome page <Router> <div class="app-containter"> - <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> + <Header /> <main> <Routes fallback=|| "Page not found.".into_view()> <Route path=StaticSegment("/") view=Home /> @@ -110,6 +49,7 @@ pub fn App() -> impl IntoView { <Route path=StaticSegment("/aes") view=AesPage /> </Routes> </main> + <Footer /> </div> </Router> } diff --git a/web/src/pages/footer.rs b/web/src/pages/footer.rs new file mode 100644 index 0000000..35a88d2 --- /dev/null +++ b/web/src/pages/footer.rs @@ -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> + } +} diff --git a/web/src/pages/header.rs b/web/src/pages/header.rs new file mode 100644 index 0000000..c48a1de --- /dev/null +++ b/web/src/pages/header.rs @@ -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) + } +} diff --git a/web/src/pages/mod.rs b/web/src/pages/mod.rs index 06fe82c..19183cc 100644 --- a/web/src/pages/mod.rs +++ b/web/src/pages/mod.rs @@ -1,3 +1,5 @@ pub mod aes; pub mod des; +pub mod footer; +pub mod header; pub mod home; diff --git a/web/style/main.scss b/web/style/main.scss index 8c86354..6724256 100644 --- a/web/style/main.scss +++ b/web/style/main.scss @@ -27,6 +27,7 @@ $l-hl-low: #f4ede8; $l-hl-high: #cecacd; $control-height: 46px; +$trans-speed: 0.3s ease; :root, body.dark-theme { @@ -66,20 +67,29 @@ body.light-theme { --focus-ring: #{rgba($l-rose, 0.3)}; } +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + body { font-family: "Inter", "Segoe UI", sans-serif; background-color: var(--bg-body); color: var(--text-main); margin: 0; transition: - background-color 0.3s, - color 0.3s; + background-color $trans-speed, + color $trans-speed; } .app-containter { + display: flex; + flex-direction: column; + min-height: 100vh; max-width: 800px; margin: 0 auto; - min-height: 100vh; } .main-nav { @@ -89,6 +99,9 @@ body { display: flex; justify-content: space-between; align-items: center; + transition: + background-color $trans-speed, + border-color $trans-speed; ul { list-style: none; @@ -111,6 +124,13 @@ body { } } +main { + flex: 1; + padding: 2rem; + width: 100%; + box-sizing: border-box; +} + .theme-toggle { background: transparent; border: 1px solid var(--border); @@ -135,11 +155,13 @@ body { text-align: center; font-size: 0.9rem; color: var(--text-muted); + transition: + background-color $trans-speed, + border-color $trans-speed, + color $trans-speed; } .footer-content { - max-width: 800px; - margin: 0 auto; padding: 0 1rem; p { @@ -167,16 +189,16 @@ body { } } -main { - padding: 2rem; -} - .cipher-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 2rem; 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 { @@ -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 { background: transparent; border: none;