mirror of
https://github.com/kristoferssolo/cipher-workshop.git
synced 2025-12-20 11:04:38 +00:00
feat(web): add footer
This commit is contained in:
parent
a93ff3f920
commit
898d5f7195
@ -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
23
web/src/pages/footer.rs
Normal 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
70
web/src/pages/header.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user