Basic functions, Home and Community Tab
This commit is contained in:
62
src/app.rs
Normal file
62
src/app.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Title};
|
||||
use leptos_router::components::{Route, Router, Routes, A};
|
||||
use leptos_router::path;
|
||||
use crate::pages::{home::HomePage, community::CommunityPage};
|
||||
use crate::backend::logic::get_server_time;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
// WICHTIG: Der Schrägstrich / vor pkg sorgt dafür, dass das CSS überall geladen wird
|
||||
<link rel="stylesheet" id="leptos" href="/pkg/bytemalte_de.css"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options=options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body><App/></body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let time = Resource::new(|| (), |_| async move { get_server_time().await });
|
||||
|
||||
view! {
|
||||
<Title text="Malxtes Community"/>
|
||||
<Router>
|
||||
<header class="navbar">
|
||||
<nav class="nav-content">
|
||||
<div class="nav-logo">"Malxte."</div>
|
||||
<div class="nav-links">
|
||||
<A href="/" exact=true>"Home"</A>
|
||||
<A href="/community">"Community"</A>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="page-content">
|
||||
<Routes fallback=|| "404">
|
||||
<Route path=path!("") view=HomePage/>
|
||||
<Route path=path!("/community") view=CommunityPage/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer">
|
||||
<Suspense fallback=|| view! { <span class="server-time">"Verbinde..."</span> }>
|
||||
{move || time.get().map(|t| view! {
|
||||
<span class="server-time">
|
||||
"Server Status: Online • " {t.unwrap_or_default()}
|
||||
</span>
|
||||
})}
|
||||
</Suspense>
|
||||
</footer>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
10
src/backend/db.rs
Normal file
10
src/backend/db.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn build_db_pool() -> SqlitePool {
|
||||
let database_url = "sqlite:community.db";
|
||||
let pool = SqlitePool::connect(database_url).await.expect("DB Fehler");
|
||||
sqlx::migrate!("./migrations").run(&pool).await.expect("Migration Fehler");
|
||||
pool
|
||||
}
|
||||
45
src/backend/logic.rs
Normal file
45
src/backend/logic.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use leptos::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub username: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn login_or_register(user: String, pass: String) -> Result<i64, ServerFnError> {
|
||||
use sqlx::SqlitePool;
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
let pool = use_context::<SqlitePool>().unwrap();
|
||||
let existing = sqlx::query!("SELECT id, password_hash FROM users WHERE username = ?", user).fetch_optional(&pool).await?;
|
||||
|
||||
if let Some(u) = existing {
|
||||
if verify(&pass, &u.password_hash).unwrap_or(false) { Ok(u.id.expect("ID fehlt")) }
|
||||
else { Err(ServerFnError::new("Falsches Passwort")) }
|
||||
} else {
|
||||
let hashed = hash(&pass, DEFAULT_COST).unwrap();
|
||||
let id = sqlx::query!("INSERT INTO users (username, password_hash) VALUES (?, ?)", user, hashed)
|
||||
.execute(&pool).await?.last_insert_rowid();
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn send_message(user_id: i64, content: String) -> Result<(), ServerFnError> {
|
||||
let pool = use_context::<sqlx::SqlitePool>().unwrap();
|
||||
sqlx::query!("INSERT INTO messages (user_id, content) VALUES (?, ?)", user_id, content).execute(&pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_all_messages() -> Result<Vec<ChatMessage>, ServerFnError> {
|
||||
let pool = use_context::<sqlx::SqlitePool>().unwrap();
|
||||
let rows = sqlx::query!("SELECT u.username, m.content FROM messages m JOIN users u ON m.user_id = u.id ORDER BY m.created_at ASC").fetch_all(&pool).await?;
|
||||
Ok(rows.into_iter().map(|r| ChatMessage { username: r.username, content: r.content }).collect())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_server_time() -> Result<String, ServerFnError> {
|
||||
Ok(chrono::Local::now().to_rfc3339())
|
||||
}
|
||||
2
src/backend/mod.rs
Normal file
2
src/backend/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod db; // Entferne das #[cfg(feature = "ssr")]
|
||||
pub mod logic; // Entferne das #[cfg(feature = "ssr")]
|
||||
12
src/lib.rs
Normal file
12
src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
pub mod app;
|
||||
pub mod pages;
|
||||
pub mod backend;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::*;
|
||||
use leptos::prelude::*;
|
||||
console_error_panic_hook::set_once();
|
||||
hydrate_body(App);
|
||||
}
|
||||
42
src/main.rs
Normal file
42
src/main.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use bytemalte_de::app::*;
|
||||
use bytemalte_de::backend::db::build_db_pool;
|
||||
|
||||
// Konfiguration laden
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options.clone(); // Klonen für den Router
|
||||
let addr = leptos_options.site_addr; // Adresse kopieren, bevor wir verschieben
|
||||
|
||||
// Datenbank vorbereiten
|
||||
let db_pool = build_db_pool().await;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let app = Router::new()
|
||||
.leptos_routes_with_context(
|
||||
&leptos_options,
|
||||
routes,
|
||||
{
|
||||
let pool = db_pool.clone();
|
||||
move || { provide_context(pool.clone()); }
|
||||
},
|
||||
{
|
||||
let opts = leptos_options.clone();
|
||||
move || shell(opts.clone())
|
||||
}
|
||||
)
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// Server starten mit der vorher gespeicherten Adresse
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
leptos::logging::log!("Server läuft auf http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service()).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {}
|
||||
144
src/pages/community.rs
Normal file
144
src/pages/community.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::html::Div;
|
||||
use crate::backend::logic::*;
|
||||
|
||||
#[component]
|
||||
pub fn CommunityPage() -> impl IntoView {
|
||||
// --- STATE & SIGNALS ---
|
||||
// Wer ist eingeloggt? (ID und Benutzername)
|
||||
let (user_id, set_user_id) = signal(None::<i64>);
|
||||
let (username, set_username) = signal(String::new());
|
||||
|
||||
// Referenz auf das Chat-Fenster für automatisches Scrollen
|
||||
let scroll_ref = NodeRef::<Div>::new();
|
||||
|
||||
// --- DATEN-ABFRAGE (RESOURCE) ---
|
||||
// Holt die Nachrichten vom Server
|
||||
let messages = Resource::new(|| (), |_| async move {
|
||||
get_all_messages().await.unwrap_or_default()
|
||||
});
|
||||
|
||||
// --- ECHTZEIT-POLLING ---
|
||||
// Fragt alle 2 Sekunden den Server nach neuen Nachrichten (nur im Browser)
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use std::time::Duration;
|
||||
Effect::new(move |_| {
|
||||
let _ = set_interval_with_handle(move || {
|
||||
messages.refetch();
|
||||
}, Duration::from_secs(2));
|
||||
});
|
||||
}
|
||||
|
||||
// --- AUTO-SCROLL EFFEKT ---
|
||||
// Immer wenn neue Nachrichten geladen werden, scrollen wir nach ganz unten
|
||||
Effect::new(move |_| {
|
||||
messages.get(); // Reagiere auf Änderungen der Nachrichten
|
||||
if let Some(div) = scroll_ref.get() {
|
||||
request_animation_frame(move || {
|
||||
div.set_scroll_top(div.scroll_height());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- SERVER ACTIONS ---
|
||||
let login_action = ServerAction::<LoginOrRegister>::new();
|
||||
let send_action = ServerAction::<SendMessage>::new();
|
||||
|
||||
// --- LOGIK-EFFEKTE ---
|
||||
// 1. Login-Erfolg verarbeiten
|
||||
Effect::new(move |_| {
|
||||
if let Some(Ok(id)) = login_action.value().get() {
|
||||
set_user_id.set(Some(id));
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Nach dem Senden sofort Nachrichten aktualisieren
|
||||
Effect::new(move |_| {
|
||||
if send_action.value().get().is_some() {
|
||||
messages.refetch();
|
||||
}
|
||||
});
|
||||
|
||||
// --- VIEW / UI ---
|
||||
view! {
|
||||
<div class="community-container">
|
||||
{move || match user_id.get() {
|
||||
// FALL A: Nutzer ist nicht eingeloggt (Anmelde-Karte)
|
||||
None => view! {
|
||||
<div class="auth-card">
|
||||
<h2>"Community Login"</h2>
|
||||
<p>"Tritt der Diskussion bei und schreibe mit Malxte."</p>
|
||||
<ActionForm action=login_action>
|
||||
<input
|
||||
type="text"
|
||||
name="user"
|
||||
placeholder="Dein Name"
|
||||
on:input:target=move |ev| set_username.set(ev.target().value())
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
name="pass"
|
||||
placeholder="Passwort"
|
||||
required
|
||||
/>
|
||||
<button type="submit">"Beitreten"</button>
|
||||
</ActionForm>
|
||||
|
||||
// Fehlermeldung anzeigen, falls Login fehlschlägt
|
||||
{move || login_action.value().get().map(|v| match v {
|
||||
Err(e) => view! { <p style="color: red; margin-top: 10px;">{e.to_string()}</p> }.into_any(),
|
||||
_ => view! {}.into_any()
|
||||
})}
|
||||
</div>
|
||||
}.into_any(),
|
||||
|
||||
// FALL B: Nutzer ist eingeloggt (Chat-Fenster)
|
||||
Some(id) => view! {
|
||||
<div class="chat-main">
|
||||
<header class="chat-top-bar">
|
||||
<h3>"Globaler Chat"</h3>
|
||||
<span class="badge">{move || username.get()}</span>
|
||||
</header>
|
||||
|
||||
<div class="message-area" node_ref=scroll_ref>
|
||||
<Suspense fallback=|| view! { <div class="loading">"Lade Nachrichten..."</div> }>
|
||||
{move || messages.get().map(|list| {
|
||||
list.into_iter().map(|msg| {
|
||||
let is_me = msg.username == username.get();
|
||||
view! {
|
||||
<div class=if is_me { "msg-row me" } else { "msg-row other" }>
|
||||
<div class="msg-bubble">
|
||||
<strong>{msg.username}</strong>
|
||||
<p>{msg.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}).collect_view()
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-wrapper">
|
||||
<ActionForm action=send_action>
|
||||
// Die ID wird versteckt mitgesendet
|
||||
<input type="hidden" name="user_id" value=id.to_string() />
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
name="content"
|
||||
placeholder="Deine Nachricht..."
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="submit">"Senden"</button>
|
||||
</div>
|
||||
</ActionForm>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
9
src/pages/dashboard.rs
Normal file
9
src/pages/dashboard.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn DashboardPage() -> impl IntoView {
|
||||
view! {
|
||||
<h1>"Dashboard"</h1>
|
||||
<p>"Hier werden später Community-Statistiken stehen."</p>
|
||||
}
|
||||
}
|
||||
11
src/pages/home.rs
Normal file
11
src/pages/home.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="home-hero">
|
||||
<h1>"Malxte."</h1>
|
||||
<p>"Willkommen in der Rust-Community."</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
2
src/pages/mod.rs
Normal file
2
src/pages/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod home;
|
||||
pub mod community; // Das hat gefehlt!
|
||||
Reference in New Issue
Block a user