Ready Home Tab
This commit is contained in:
@@ -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"]
|
||||
|
||||
BIN
public/assets/Logo.jpg
Normal file
BIN
public/assets/Logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
public/assets/bytemalte_de.png
Normal file
BIN
public/assets/bytemalte_de.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
BIN
public/assets/malxte_de.png
Normal file
BIN
public/assets/malxte_de.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -29,11 +29,11 @@ pub fn App() -> impl IntoView {
|
||||
let time = Resource::new(|| (), |_| async move { get_server_time().await });
|
||||
|
||||
view! {
|
||||
<Title text="Malxtes Community"/>
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod home;
|
||||
pub mod community; // Das hat gefehlt!
|
||||
pub mod community;
|
||||
|
||||
127
style/home.scss
Normal file
127
style/home.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user