Ready Home Tab

This commit is contained in:
Malte Schröder
2025-12-18 15:03:27 +01:00
parent e7f1b4b566
commit 9dba343301
11 changed files with 284 additions and 25 deletions

View File

@@ -14,12 +14,14 @@ chrono = { version = "0.4.42", features = ["clock"] }
getrandom = { version = "0.3", features = ["wasm_js"] } getrandom = { version = "0.3", features = ["wasm_js"] }
bcrypt = { version = "0.17", optional = true } bcrypt = { version = "0.17", optional = true }
console_error_panic_hook = { version = "0.1", 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 } axum = { version = "0.8.0", optional = true }
leptos_axum = { version = "0.8.0", optional = true } leptos_axum = { version = "0.8.0", optional = true }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true } tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
sqlx = { version = "0.8.2", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true } sqlx = { version = "0.8.2", features = ["runtime-tokio-rustls", "sqlite", "chrono"], optional = true }
serde = "1.0.228" serde = "1.0.228"
web-sys = { version = "0.3.83", features = ["Window", "Document", "Element", "HtmlElement"] }
gloo-events = "0.1"
[features] [features]
hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"] hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"]

BIN
public/assets/Logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

BIN
public/assets/malxte_de.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -29,11 +29,11 @@ pub fn App() -> impl IntoView {
let time = Resource::new(|| (), |_| async move { get_server_time().await }); let time = Resource::new(|| (), |_| async move { get_server_time().await });
view! { view! {
<Title text="Malxtes Community"/> <Title text="Bytemalte Community"/>
<Router> <Router>
<header class="navbar"> <header class="navbar">
<nav class="nav-content"> <nav class="nav-content">
<div class="nav-logo">"Malxte."</div> <div class="nav-logo">"Bytemalte."</div>
<div class="nav-links"> <div class="nav-links">
<A href="/" exact=true>"Home"</A> <A href="/" exact=true>"Home"</A>
<A href="/community">"Community"</A> <A href="/community">"Community"</A>

View File

@@ -68,7 +68,7 @@ pub fn CommunityPage() -> impl IntoView {
None => view! { None => view! {
<div class="auth-card"> <div class="auth-card">
<h2>"Community Login"</h2> <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> <ActionForm action=login_action>
<input <input
type="text" type="text"

View File

@@ -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>
}
}

View File

@@ -1,11 +1,155 @@
use leptos::prelude::*; 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] #[component]
pub fn HomePage() -> impl IntoView { 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! { view! {
<div class="home-hero"> <div class="home-container">
<h1>"Malxte."</h1> <header class="home-hero">
<p>"Willkommen in der Rust-Community."</p> <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> </div>
} }
} }

View File

@@ -1,2 +1,2 @@
pub mod home; pub mod home;
pub mod community; // Das hat gefehlt! pub mod community;

127
style/home.scss Normal file
View File

@@ -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;
}
}

View File

@@ -1,3 +1,6 @@
@import "home";
:root { :root {
--primary: #6366f1; --primary: #6366f1;
--primary-dark: #4f46e5; --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 / LOGIN ---
.community-container { .community-container {
display: flex; justify-content: center; padding: 3rem 1rem; display: flex; justify-content: center; padding: 3rem 1rem;