diff --git a/Cargo.toml b/Cargo.toml index 83689a8..7ffa74b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,14 @@ chrono = { version = "0.4.42", features = ["clock"] } getrandom = { version = "0.3", features = ["wasm_js"] } bcrypt = { version = "0.17", optional = true } console_error_panic_hook = { version = "0.1", optional = true } -wasm-bindgen = { version = "=0.2.105", optional = true } +wasm-bindgen = { version = "0.2.106", optional = true } axum = { version = "0.8.0", optional = true } leptos_axum = { version = "0.8.0", optional = true } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } sqlx = { version = "0.8.2", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true } serde = "1.0.228" +web-sys = { version = "0.3.83", features = ["Window", "Document", "Element", "HtmlElement"] } +gloo-events = "0.1" [features] hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"] diff --git a/public/assets/Logo.jpg b/public/assets/Logo.jpg new file mode 100644 index 0000000..aee2d26 Binary files /dev/null and b/public/assets/Logo.jpg differ diff --git a/public/assets/bytemalte_de.png b/public/assets/bytemalte_de.png new file mode 100644 index 0000000..5966360 Binary files /dev/null and b/public/assets/bytemalte_de.png differ diff --git a/public/assets/malxte_de.png b/public/assets/malxte_de.png new file mode 100644 index 0000000..57c67fa Binary files /dev/null and b/public/assets/malxte_de.png differ diff --git a/src/app.rs b/src/app.rs index c2a480c..acc5885 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,11 +29,11 @@ pub fn App() -> impl IntoView { let time = Resource::new(|| (), |_| async move { get_server_time().await }); view! { - + <Title text="Bytemalte Community"/> <Router> <header class="navbar"> <nav class="nav-content"> - <div class="nav-logo">"Malxte."</div> + <div class="nav-logo">"Bytemalte."</div> <div class="nav-links"> <A href="/" exact=true>"Home"</A> <A href="/community">"Community"</A> diff --git a/src/pages/community.rs b/src/pages/community.rs index 9eaa688..0b597ad 100644 --- a/src/pages/community.rs +++ b/src/pages/community.rs @@ -68,7 +68,7 @@ pub fn CommunityPage() -> impl IntoView { None => view! { <div class="auth-card"> <h2>"Community Login"</h2> - <p>"Tritt der Diskussion bei und schreibe mit Malxte."</p> + <p>"Tritt der Diskussion bei und schreibe mit anderen der Bytemalte Community."</p> <ActionForm action=login_action> <input type="text" diff --git a/src/pages/dashboard.rs b/src/pages/dashboard.rs deleted file mode 100644 index 2714d02..0000000 --- a/src/pages/dashboard.rs +++ /dev/null @@ -1,9 +0,0 @@ -use leptos::prelude::*; - -#[component] -pub fn DashboardPage() -> impl IntoView { - view! { - <h1>"Dashboard"</h1> - <p>"Hier werden später Community-Statistiken stehen."</p> - } -} \ No newline at end of file diff --git a/src/pages/home.rs b/src/pages/home.rs index 384d69c..1cf2231 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,11 +1,155 @@ use leptos::prelude::*; +use leptos::html::Div; +use leptos::wasm_bindgen::JsCast; +use web_sys::HtmlElement; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub struct Project { + pub id: usize, + pub title: String, + pub description: String, + pub image_url: String, + pub download_url: String, + pub code_url: String, +} + +fn get_all_projects() -> Vec<Project> { + vec![ + Project { + id: 1, + title: "Malxte.de Rust Website".into(), + description: "Eine voll in Rust mit Yew Programmierte Website mit allen Links und allem über mich.".into(), + image_url: "/assets/malxte_de.png".into(), + download_url: "https://github.com/.../releases".into(), // Dein Link + code_url: "https://gitea.malxte.de/Bytemalte/malxte_de.git".into(), // Dein Code Link + }, + Project { + id: 2, + title: "Bytemalte.de Community Website".into(), + description: "Die Webseite auf der du dich gerade befindest, gebaut mit Leptos für die Bytemalte Community.".into(), + image_url: "/assets/bytemalte_de.png".into(), + download_url: "#".into(), + code_url: "https://gitea.malxte.de/Bytemalte/bytemalte_de".into(), + }, + Project { + id: 3, + title: "Logo Design".into(), + description: "Das offizielle Bytemalte Logo. (Nicht frei verwendbar!)".into(), + image_url: "/assets/Logo.jpg".into(), // JPG geht natürlich auch + download_url: "/assets/Logo.jpg".into(), // Man kann Bilder auch direkt verlinken + code_url: "#".into(), + }, + ] +} #[component] pub fn HomePage() -> impl IntoView { + let (projects, set_projects) = signal(Vec::<Project>::new()); + let (page, set_page) = signal(1); + let (loading, set_loading) = signal(false); + let (all_loaded, set_all_loaded) = signal(false); + + let items_per_page = 3; // Wie viele Projekte pro Scroll-Schub geladen werden + let sentinel = NodeRef::<Div>::new(); + + let fetch_projects = move |p: usize| { + if all_loaded.get_untracked() { return; } + + set_loading.set(true); + + // Simuliert eine kurze Ladezeit für das Feeling + set_timeout(move || { + let all_my_projects = get_all_projects(); + let start = (p - 1) * items_per_page; + let end = (start + items_per_page).min(all_my_projects.len()); + + if start >= all_my_projects.len() { + set_all_loaded.set(true); + set_loading.set(false); + return; + } + + let slice = &all_my_projects[start..end]; + set_projects.update(|projs| projs.extend(slice.to_vec())); + + if end >= all_my_projects.len() { + set_all_loaded.set(true); + } + set_loading.set(false); + }, std::time::Duration::from_millis(400)); + }; + + // Initialer Load + Effect::new(move |_| { + fetch_projects(page.get()); + }); + + // Scroll-Event-Listener + Effect::new(move |_| { + let w = window(); + let w_clone = w.clone(); + + let handle = gloo_events::EventListener::new(&w, "scroll", move |_| { + if all_loaded.get_untracked() || loading.get_untracked() { return; } + + if let Some(doc) = w_clone.document() { + if let Some(el) = doc.document_element().and_then(|e| e.dyn_into::<HtmlElement>().ok()) { + let scroll_y = w_clone.scroll_y().unwrap_or(0.0); + let inner_height = w_clone.inner_height().unwrap().as_f64().unwrap_or(0.0); + let total_height = el.offset_height() as f64; + + if scroll_y + inner_height >= total_height - 200.0 { + set_page.update(|p| *p += 1); + fetch_projects(page.get_untracked()); + } + } + } + }); + handle.forget(); + }); + view! { - <div class="home-hero"> - <h1>"Malxte."</h1> - <p>"Willkommen in der Rust-Community."</p> + <div class="home-container"> + <header class="home-hero"> + <h1>"Bytemalte."</h1> + <p>"Entdecke meine neuesten Projekte - Bytemalte."</p> + </header> + + <main class="projects-grid"> + <For + each=move || projects.get() + key=|p| p.id + children=|p| { + view! { + <div class="project-card"> + <div class="project-image"> + <img src=p.image_url alt=p.title.clone() /> + </div> + <div class="project-content"> + <h3>{p.title}</h3> + <p>{p.description}</p> + <div class="project-links"> + <a href=p.download_url target="_blank" class="btn-download">"Download"</a> + <a href=p.code_url target="_blank" class="btn-code">"Source Code"</a> + </div> + </div> + </div> + } + } + /> + </main> + + <div node_ref=sentinel class="loading-trigger"> + {move || { + if loading.get() { + view! { <div class="spinner"></div> }.into_any() + } else if all_loaded.get() { + view! { <p class="end-msg">"Das waren alle Projekte ✨"</p> }.into_any() + } else { + view! { <p>"Mehr laden..."</p> }.into_any() + } + }} + </div> </div> } } \ No newline at end of file diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 231e7a6..1826558 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,2 +1,2 @@ pub mod home; -pub mod community; // Das hat gefehlt! \ No newline at end of file +pub mod community; diff --git a/style/home.scss b/style/home.scss new file mode 100644 index 0000000..6f0fec9 --- /dev/null +++ b/style/home.scss @@ -0,0 +1,127 @@ +.home-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; +} + +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.project-card { + background: var(--white); + border-radius: 1.25rem; + overflow: hidden; + border: 1px solid var(--border); + transition: transform 0.3s ease, box-shadow 0.3s ease; + display: flex; + flex-direction: column; + + &:hover { + transform: translateY(-8px); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + } + + .project-image { + width: 100%; + height: 200px; + background: #eef2ff; + overflow: hidden; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; + } + } + + &:hover .project-image img { + transform: scale(1.05); + } + + .project-content { + padding: 1.5rem; + display: flex; + flex-direction: column; + flex: 1; + + h3 { + margin: 0 0 0.75rem 0; + font-size: 1.25rem; + color: var(--text-main); + } + + p { + margin: 0 0 1.5rem 0; + color: var(--text-light); + font-size: 0.95rem; + line-height: 1.6; + flex: 1; + } + } + + .project-links { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + + a { + text-decoration: none; + padding: 0.6rem; + border-radius: 0.75rem; + font-size: 0.85rem; + font-weight: 600; + text-align: center; + transition: all 0.2s; + } + + .btn-download { + background: var(--primary); + color: white; + &:hover { background: var(--primary-dark); } + } + + .btn-code { + background: #f1f5f9; + color: var(--text-main); + &:hover { background: #e2e8f0; } + } + } +} + +.loading-trigger { + display: flex; + justify-content: center; + align-items: center; + min-height: 150px; + padding-bottom: 50px; + color: var(--text-light); + + p { font-size: 0.9rem; opacity: 0.7; } +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(99, 102, 241, 0.1); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@media (max-width: 640px) { + .projects-grid { + grid-template-columns: 1fr; + } + .home-hero h1 { + font-size: 2.5rem; + } +} \ No newline at end of file diff --git a/style/main.scss b/style/main.scss index 1e5803d..6025343 100644 --- a/style/main.scss +++ b/style/main.scss @@ -1,3 +1,6 @@ +@import "home"; + + :root { --primary: #6366f1; --primary-dark: #4f46e5; @@ -45,14 +48,6 @@ body { } } -// --- HOME PAGE --- -.home-hero { - text-align: center; - padding: 100px 20px; - h1 { font-size: 4rem; color: var(--primary); margin-bottom: 1rem; font-weight: 900; } - p { font-size: 1.25rem; color: var(--text-light); } -} - // --- COMMUNITY / LOGIN --- .community-container { display: flex; justify-content: center; padding: 3rem 1rem;