Compare commits

..

5 Commits

Author SHA1 Message Date
spinline
09a4c69282 fix(auth): fix MsgPack serialization by adding missing feature in backend and reverting to individual arguments
All checks were successful
Build MIPS Binary / build (push) Successful in 5m15s
2026-02-11 22:03:46 +03:00
spinline
a877e0c393 chore: remove db files from git and update gitignore
All checks were successful
Build MIPS Binary / build (push) Successful in 5m13s
2026-02-11 21:54:15 +03:00
spinline
fd65df2962 fix(setup): add logging and error details for setup debugging
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
2026-02-11 21:53:53 +03:00
spinline
9d160a7ef5 fix(ui): use thread_local for toast signal to fix context issue
All checks were successful
Build MIPS Binary / build (push) Successful in 5m12s
2026-02-11 21:43:06 +03:00
spinline
a24e4101e8 feat(ui): toast entegrasyonu (sonner)
All checks were successful
Build MIPS Binary / build (push) Successful in 5m13s
2026-02-11 21:32:39 +03:00
16 changed files with 319 additions and 38 deletions

6
.gitignore vendored
View File

@@ -8,3 +8,9 @@ backend.log
.runner .runner
.env .env
backend/.env backend/.env
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite-shm
*.sqlite-wal

1
Cargo.lock generated
View File

@@ -3761,6 +3761,7 @@ dependencies = [
"struct-patch", "struct-patch",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing",
"utoipa", "utoipa",
] ]

View File

@@ -44,6 +44,6 @@ tower_governor = "0.8.0"
governor = "0.10.4" governor = "0.10.4"
# Leptos # Leptos
leptos = { version = "0.8.15", features = ["nightly"] } leptos = { version = "0.8.15", features = ["nightly", "msgpack"] }
leptos_axum = { version = "0.8.7" } leptos_axum = { version = "0.8.7" }
jsonwebtoken = "9" jsonwebtoken = "9"

View File

@@ -38,4 +38,7 @@ struct-patch = "0.5"
# Rust/UI Components # Rust/UI Components
leptos_ui = "0.3" leptos_ui = "0.3"
tw_merge = "0.1" tw_merge = "0.1"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
[package.metadata.leptos]
tailwind-input-file = "input.css"

View File

@@ -1,14 +1,13 @@
{ {
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "tailwind.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "", "author": "",
"license": "ISC", "dependencies": {
"@tailwindcss/cli": "^4.1.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"description": "",
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
@@ -17,11 +16,13 @@
"postcss-preset-env": "^10.1.3", "postcss-preset-env": "^10.1.3",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"dependencies": { "keywords": [],
"@tailwindcss/cli": "^4.1.18", "license": "ISC",
"class-variance-authority": "^0.7.1", "main": "tailwind.config.js",
"clsx": "^2.1.1", "name": "frontend",
"tailwind-merge": "^3.4.0", "scripts": {
"tailwindcss-animate": "^1.0.7" "test": "echo \"Error: no test specified\" && exit 1"
} },
} "type": "module",
"version": "1.0.0"
}

View File

@@ -6,20 +6,20 @@ use leptos::prelude::*;
use leptos::task::spawn_local; use leptos::task::spawn_local;
use leptos_router::components::{Router, Routes, Route}; use leptos_router::components::{Router, Routes, Route};
use leptos_router::hooks::use_navigate; use leptos_router::hooks::use_navigate;
use crate::components::toast::Toaster; use crate::components::ui::toast::Toaster;
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
crate::components::ui::toast::provide_toaster();
view! { view! {
<InnerApp />
<Toaster /> <Toaster />
<InnerApp />
} }
} }
#[component] #[component]
fn InnerApp() -> impl IntoView { fn InnerApp() -> impl IntoView {
crate::store::provide_torrent_store(); crate::store::provide_torrent_store();
crate::components::toast::provide_toast_context();
let store = use_context::<crate::store::TorrentStore>(); let store = use_context::<crate::store::TorrentStore>();
let is_loading = signal(true); let is_loading = signal(true);

View File

@@ -41,7 +41,8 @@ pub fn Setup() -> impl IntoView {
} }
Err(e) => { Err(e) => {
log::error!("Setup failed: {:?}", e); log::error!("Setup failed: {:?}", e);
error.1.set(Some("Kurulum sırasında bir hata oluştu".to_string())); // Hatanın sadece mesaj kısmını almaya çalışalım, yoksa full struct basılabilir
error.1.set(Some(format!("Hata: {}", e)));
loading.1.set(false); loading.1.set(false);
} }
} }

View File

@@ -2,5 +2,5 @@ pub mod context_menu;
pub mod layout; pub mod layout;
pub mod torrent; pub mod torrent;
pub mod auth; pub mod auth;
pub mod toast; // pub mod toast; (Removed)
pub mod ui; pub mod ui;

View File

@@ -1,3 +1,4 @@
pub mod button; pub mod button;
pub mod card; pub mod card;
pub mod input; pub mod input;
pub mod toast;

View File

@@ -0,0 +1,232 @@
use leptos::prelude::*;
use tw_merge::*;
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display, Debug)]
pub enum ToastType {
#[default]
Default,
Success,
Error,
Warning,
Info,
Loading,
}
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display, Debug)]
pub enum SonnerPosition {
TopLeft,
TopCenter,
TopRight,
#[default]
BottomRight,
BottomCenter,
BottomLeft,
}
#[derive(Clone, Copy, PartialEq, Eq, Default, strum::Display, Debug)]
pub enum SonnerDirection {
TopDown,
#[default]
BottomUp,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToastData {
pub id: u64,
pub title: String,
pub description: Option<String>,
pub variant: ToastType,
pub duration: u64, // ms
}
#[derive(Clone, Copy)]
pub struct ToasterStore {
pub toasts: RwSignal<Vec<ToastData>>,
}
#[component]
pub fn SonnerTrigger(
#[prop(into, optional)] class: String,
#[prop(optional, default = ToastType::default())] variant: ToastType,
#[prop(into)] title: String,
description: Option<String>,
#[prop(into, optional)] position: String,
on_dismiss: Option<Callback<()>>,
) -> impl IntoView {
let variant_classes = match variant {
ToastType::Default => "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
ToastType::Success => "bg-green-500 text-white hover:bg-green-600",
ToastType::Error => "bg-red-500 text-white shadow-xs hover:bg-red-600",
ToastType::Warning => "bg-yellow-500 text-white hover:bg-yellow-600",
ToastType::Info => "bg-blue-500 text-white shadow-xs hover:bg-blue-600",
ToastType::Loading => "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
};
let merged_class = tw_merge!(
"inline-flex flex-col items-start justify-center gap-1 min-w-[300px] rounded-md text-sm font-medium transition-all shadow-lg p-4 cursor-pointer pointer-events-auto border border-border/50",
variant_classes,
class
);
// Only set position attribute if not empty
let position_attr = if position.is_empty() { None } else { Some(position) };
// Clone title for data attribute usage, original moved into view
let title_clone = title.clone();
view! {
<div
class=merged_class
data-name="SonnerTrigger"
data-variant=variant.to_string()
data-toast-title=title_clone
data-toast-position=position_attr
on:click=move |_| {
if let Some(cb) = on_dismiss {
cb.run(());
}
}
>
<div class="font-semibold">{title}</div>
{move || description.as_ref().map(|d| view! { <div class="text-xs opacity-90">{d.clone()}</div> })}
</div>
}
}
#[component]
pub fn SonnerContainer(
children: Children,
#[prop(into, optional)] class: String,
#[prop(optional, default = SonnerPosition::default())] position: SonnerPosition,
) -> impl IntoView {
let merged_class = tw_merge!("toast__container fixed z-[9999] flex flex-col gap-2 p-4 outline-none pointer-events-none", class);
view! {
<div class=merged_class data-position=position.to_string()>
{children()}
</div>
}
}
#[component]
pub fn SonnerList(
children: Children,
#[prop(into, optional)] class: String,
#[prop(optional, default = SonnerPosition::default())] position: SonnerPosition,
#[prop(optional, default = SonnerDirection::default())] direction: SonnerDirection,
#[prop(into, default = "false".to_string())] expanded: String,
#[prop(into, optional)] style: String,
) -> impl IntoView {
let merged_class = tw_merge!(
"contents",
class
);
view! {
<div
class=merged_class
data-name="SonnerList"
data-sonner-toaster="true"
data-sonner-theme="light"
data-position=position.to_string()
data-expanded=expanded
data-direction=direction.to_string()
style=style
>
{children()}
</div>
}
}
// Thread local storage for global access without Context
thread_local! {
static TOASTS: std::cell::RefCell<Option<RwSignal<Vec<ToastData>>>> = std::cell::RefCell::new(None);
}
pub fn provide_toaster() {
let toasts = RwSignal::new(Vec::<ToastData>::new());
// Set global thread_local
TOASTS.with(|t| *t.borrow_mut() = Some(toasts));
// Also provide context for components
provide_context(ToasterStore { toasts });
}
#[component]
pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosition) -> impl IntoView {
// Global store'u al
let store = use_context::<ToasterStore>().expect("Toaster context not found. Call provide_toaster() in App root.");
let toasts = store.toasts;
// Auto-derive direction from position
let direction = match position {
SonnerPosition::TopLeft | SonnerPosition::TopCenter | SonnerPosition::TopRight => SonnerDirection::TopDown,
_ => SonnerDirection::BottomUp,
};
let container_class = match position {
SonnerPosition::TopLeft => "left-0 top-0 items-start",
SonnerPosition::TopRight => "right-0 top-0 items-end",
SonnerPosition::TopCenter => "left-1/2 -translate-x-1/2 top-0 items-center",
SonnerPosition::BottomCenter => "left-1/2 -translate-x-1/2 bottom-0 items-center",
SonnerPosition::BottomLeft => "left-0 bottom-0 items-start",
SonnerPosition::BottomRight => "right-0 bottom-0 items-end",
};
view! {
<SonnerContainer class=container_class position=position>
<SonnerList position=position direction=direction>
<For
each=move || toasts.get()
key=|toast| toast.id
children=move |toast| {
let id = toast.id;
view! {
<SonnerTrigger
variant=toast.variant
title=toast.title
description=toast.description
on_dismiss=Some(Callback::new(move |_| {
toasts.update(|vec| vec.retain(|t| t.id != id));
}))
/>
}
}
/>
</SonnerList>
</SonnerContainer>
}
}
// Global Helper Functions
pub fn toast(title: impl Into<String>, variant: ToastType) {
let signal_opt = TOASTS.with(|t| *t.borrow());
if let Some(toasts) = signal_opt {
let id = js_sys::Math::random().to_bits();
let new_toast = ToastData {
id,
title: title.into(),
description: None,
variant,
duration: 4000,
};
toasts.update(|t| t.push(new_toast.clone()));
// Auto remove after duration
let duration = new_toast.duration;
leptos::task::spawn_local(async move {
gloo_timers::future::TimeoutFuture::new(duration as u32).await;
toasts.update(|vec| vec.retain(|t| t.id != id));
});
} else {
gloo_console::warn!("ToasterStore not found (global static). Make sure provide_toaster() is called.");
}
}
pub fn toast_success(title: impl Into<String>) { toast(title, ToastType::Success); }
pub fn toast_error(title: impl Into<String>) { toast(title, ToastType::Error); }
pub fn toast_warning(title: impl Into<String>) { toast(title, ToastType::Warning); }
pub fn toast_info(title: impl Into<String>) { toast(title, ToastType::Info); }

View File

@@ -7,19 +7,21 @@ use std::collections::HashMap;
use struct_patch::traits::Patch; use struct_patch::traits::Patch;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use crate::components::toast::ToastContext; use crate::components::ui::toast::{ToastType, toast};
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) { pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
let msg = message.into(); let msg = message.into();
gloo_console::log!("TOAST CALL:", &msg, format!("{:?}", level)); gloo_console::log!("TOAST CALL:", &msg, format!("{:?}", level));
log::info!("Displaying toast: [{:?}] {}", level, msg); log::info!("Displaying toast: [{:?}] {}", level, msg);
if let Some(context) = use_context::<ToastContext>() { let variant = match level {
context.add(msg, level); NotificationLevel::Success => ToastType::Success,
} else { NotificationLevel::Error => ToastType::Error,
log::error!("ToastContext not found!"); NotificationLevel::Warning => ToastType::Warning,
gloo_console::error!("ToastContext not found!"); NotificationLevel::Info => ToastType::Info,
} };
toast(msg, variant);
} }

2
frontend/ui_config.toml Normal file
View File

@@ -0,0 +1,2 @@
base_color = "neutral"
base_path_components = "src/components"

View File

@@ -10,6 +10,7 @@ struct-patch = "0.5"
rmp-serde = "1.3" rmp-serde = "1.3"
bytes = "1" bytes = "1"
http = "1" http = "1"
tracing = "0.1"
# Leptos 0.8.7 # Leptos 0.8.7
leptos = { version = "0.8.15", features = ["nightly", "msgpack"] } leptos = { version = "0.8.15", features = ["nightly", "msgpack"] }

View File

@@ -28,8 +28,17 @@ impl Db {
} }
async fn run_migrations(&self) -> Result<()> { async fn run_migrations(&self) -> Result<()> {
sqlx::migrate!("./migrations").run(&self.pool).await?; tracing::info!("Starting database migrations...");
Ok(()) match sqlx::migrate!("./migrations").run(&self.pool).await {
Ok(_) => {
tracing::info!("Database migrations completed successfully.");
Ok(())
}
Err(e) => {
tracing::error!("Database migration failed: {}", e);
Err(e.into())
}
}
} }
// --- User Operations --- // --- User Operations ---

View File

@@ -24,9 +24,19 @@ pub struct SetupStatus {
pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> { pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
use crate::DbContext; use crate::DbContext;
let db_context = use_context::<DbContext>().ok_or_else(|| ServerFnError::new("DB Context missing"))?; tracing::info!("Checking setup status...");
let db_context = use_context::<DbContext>().ok_or_else(|| {
tracing::error!("DB Context missing in GetSetupStatus");
ServerFnError::new("DB Context missing")
})?;
let has_users = db_context.db.has_users().await let has_users = db_context.db.has_users().await
.map_err(|e| ServerFnError::new(format!("DB error: {}", e)))?; .map_err(|e| {
tracing::error!("DB error in GetSetupStatus: {}", e);
ServerFnError::new(format!("DB error: {}", e))
})?;
tracing::info!("Setup status: completed={}", has_users);
Ok(SetupStatus { Ok(SetupStatus {
completed: has_users, completed: has_users,
@@ -37,21 +47,33 @@ pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
pub async fn setup(username: String, password: String) -> Result<(), ServerFnError> { pub async fn setup(username: String, password: String) -> Result<(), ServerFnError> {
use crate::DbContext; use crate::DbContext;
let db_context = use_context::<DbContext>().ok_or_else(|| ServerFnError::new("DB Context missing"))?; tracing::info!("Attempting setup for user: {}", username);
let db_context = use_context::<DbContext>().ok_or_else(|| {
tracing::error!("DB Context missing in Setup");
ServerFnError::new("DB Context missing")
})?;
// Check if setup is already done // Check if setup is already done
let has_users = db_context.db.has_users().await.unwrap_or(false); let has_users = db_context.db.has_users().await.unwrap_or(false);
if has_users { if has_users {
tracing::warn!("Setup attempt blocked: Setup already completed");
return Err(ServerFnError::new("Setup already completed")); return Err(ServerFnError::new("Setup already completed"));
} }
// Hash password (low cost for MIPS) // Hash password (low cost for MIPS)
let password_hash = bcrypt::hash(&password, 6) let password_hash = bcrypt::hash(&password, 6)
.map_err(|_| ServerFnError::new("Hashing error"))?; .map_err(|e| {
tracing::error!("Hashing error: {}", e);
ServerFnError::new("Hashing error")
})?;
db_context.db.create_user(&username, &password_hash).await db_context.db.create_user(&username, &password_hash).await
.map_err(|e| ServerFnError::new(format!("DB error: {}", e)))?; .map_err(|e| {
tracing::error!("Failed to create user: {}", e);
ServerFnError::new(format!("DB error: {}", e))
})?;
tracing::info!("Setup completed successfully for user: {}", username);
Ok(()) Ok(())
} }

Binary file not shown.