Compare commits

...

16 Commits

Author SHA1 Message Date
spinline
fe117cdaec chore: add detailed logging for web push notifications in webhook handler
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-13 12:11:14 +03:00
spinline
e062a3c8cd feat: add internal notification endpoint for rTorrent event hooks
All checks were successful
Build MIPS Binary / build (push) Successful in 1m51s
2026-02-13 12:08:40 +03:00
spinline
ae2c9c934d fix: center toast notifications on mobile screens
All checks were successful
Build MIPS Binary / build (push) Successful in 1m54s
2026-02-13 00:08:54 +03:00
spinline
f7e1356eae fix: restrict Add Torrent dialog width for better UI
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-13 00:06:40 +03:00
spinline
98b1f059c7 feat: modernize Add Torrent dialog and perform final code cleanup of warnings and dead code
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-13 00:03:26 +03:00
spinline
a3735d0931 fix: resolve compilation errors related to JsCast and AlertDialogTrigger attributes
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-12 23:39:17 +03:00
spinline
55f00729ee fix: relocate AlertDialog outside of DropdownMenu to ensure proper centering
Some checks failed
Build MIPS Binary / build (push) Failing after 30s
2026-02-12 23:37:36 +03:00
spinline
275f4a91b2 fix: change href to src in Trunk script tag to resolve build error
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-12 23:34:06 +03:00
spinline
025a0c4a57 fix: use script tag for Trunk rust asset to resolve preload warnings
Some checks failed
Build MIPS Binary / build (push) Failing after 25s
2026-02-12 23:32:37 +03:00
spinline
b29f9f3cc2 fix: align AlertDialog structure with project standards using AlertDialogBody
All checks were successful
Build MIPS Binary / build (push) Successful in 1m52s
2026-02-12 23:30:42 +03:00
spinline
feede5c5b4 fix: resolve compilation type error and cleanup unused imports in app.rs
All checks were successful
Build MIPS Binary / build (push) Successful in 1m50s
2026-02-12 23:26:45 +03:00
spinline
c1306a32a9 fix: use data-preload='false' and revert SW strategy to resolve browser warnings
Some checks failed
Build MIPS Binary / build (push) Failing after 44s
2026-02-12 23:23:40 +03:00
spinline
ed5fba4b46 fix: further refine alert dialog styling and button layout
Some checks failed
Build MIPS Binary / build (push) Failing after 34s
2026-02-12 23:22:07 +03:00
spinline
f149603ac8 fix: improve bulk delete dialog styling and responsive layout
All checks were successful
Build MIPS Binary / build (push) Successful in 1m54s
2026-02-12 23:17:40 +03:00
spinline
89ad42f24d feat: use constant dashboard skeleton for all loading states (Standard 1)
All checks were successful
Build MIPS Binary / build (push) Successful in 12m58s
2026-02-12 23:02:36 +03:00
spinline
bec804131b feat: add cargo and node_modules caching to Gitea workflow
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
2026-02-12 22:59:37 +03:00
12 changed files with 249 additions and 237 deletions

View File

@@ -22,12 +22,32 @@ jobs:
git fetch --depth=1 origin ${{ gitea.sha }}
git checkout FETCH_HEAD
- name: Cache Cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: frontend/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Build Frontend
run: |
cd frontend
npm install
npx @tailwindcss/cli -i input.css -o public/tailwind.css --minify --content './src/**/*.rs'
# Trunk'ın optimizasyonunu kapalı (0) tutuyoruz çünkü Cargo.toml'daki opt-level='z' zaten o işi yapıyor.
trunk build --release
- name: Build Backend (MIPS)

View File

@@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
pub mod auth;
pub mod setup;
pub mod notifications;
#[derive(RustEmbed)]
#[folder = "../frontend/dist"]

View File

@@ -0,0 +1,48 @@
use axum::{
extract::{State, Query},
http::StatusCode,
};
use serde::Deserialize;
use shared::{AppEvent, SystemNotification, NotificationLevel};
use crate::AppState;
#[derive(Deserialize)]
pub struct TorrentFinishedQuery {
pub name: String,
pub hash: String,
}
pub async fn torrent_finished_handler(
State(state): State<AppState>,
Query(params): Query<TorrentFinishedQuery>,
) -> StatusCode {
tracing::info!("Torrent finished notification received: {} ({})", params.name, params.hash);
let message = format!("Torrent tamamlandı: {}", params.name);
// 1. Send to active SSE clients (for Toast)
let notification = SystemNotification {
level: NotificationLevel::Success,
message: message.clone(),
};
let _ = state.event_bus.send(AppEvent::Notification(notification));
// 2. Send Web Push Notification (for Background)
#[cfg(feature = "push-notifications")]
{
let push_store = state.push_store.clone();
let title = "Torrent Tamamlandı".to_string();
let body = message;
let torrent_name = params.name.clone();
tokio::spawn(async move {
tracing::info!("Attempting to send Web Push notification for torrent: {}", torrent_name);
match crate::push::send_push_notification(&push_store, &title, &body).await {
Ok(_) => tracing::info!("Web Push notification sent successfully for: {}", torrent_name),
Err(e) => tracing::error!("Failed to send Web Push notification for {}: {:?}", torrent_name, e),
}
});
}
StatusCode::OK
}

View File

@@ -60,6 +60,7 @@ async fn auth_middleware(
|| path.starts_with("/api/server_fns/get_setup_status")
|| path.starts_with("/api/server_fns/Setup")
|| path.starts_with("/api/server_fns/setup")
|| path.starts_with("/api/internal/")
|| path.starts_with("/swagger-ui")
|| path.starts_with("/api-docs")
|| !path.starts_with("/api/")
@@ -434,6 +435,7 @@ async fn main() {
let db_for_ctx = db.clone();
let app = app
.route("/api/events", get(sse::sse_handler))
.route("/api/internal/torrent-finished", post(handlers::notifications::torrent_finished_handler))
.route("/api/server_fns/{*fn_name}", post({
let scgi_path = scgi_path_for_ctx.clone();
let db = db_for_ctx.clone();

View File

@@ -20,7 +20,7 @@
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<!-- Trunk Assets -->
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" data-no-preload />
<script data-trunk rel="rust" src="Cargo.toml" data-wasm-opt="0" data-preload="false"></script>
<link data-trunk rel="css" href="public/tailwind.css" />
<link data-trunk rel="copy-file" href="manifest.json" />
<link data-trunk rel="copy-file" href="icon-192.png" />

View File

@@ -1,6 +1,5 @@
use crate::components::layout::protected::Protected;
use crate::components::ui::skeleton::Skeleton;
use crate::components::ui::card::{Card, CardHeader, CardContent};
use crate::components::torrent::table::TorrentTable;
use crate::components::auth::login::Login;
use crate::components::auth::setup::Setup;
@@ -42,7 +41,7 @@ pub fn App() -> impl IntoView {
fn InnerApp() -> impl IntoView {
crate::store::provide_torrent_store();
let store = use_context::<crate::store::TorrentStore>();
let loc = use_location();
let _loc = use_location();
let is_loading = signal(true);
let is_authenticated = signal(false);
@@ -146,70 +145,43 @@ fn InnerApp() -> impl IntoView {
});
view! {
<Show when=move || !is_loading.0.get() fallback=move || {
let path = loc.pathname.get();
if path == "/login" {
// Login Skeleton
view! {
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
<Card class="w-full max-w-sm shadow-lg border-none">
<CardHeader class="pb-2 items-center space-y-4">
<Skeleton class="w-12 h-12 rounded-xl" />
<Skeleton class="h-8 w-32" />
<Skeleton class="h-4 w-48" />
</CardHeader>
<CardContent class="pt-4 space-y-6">
<div class="space-y-2">
<Skeleton class="h-4 w-24" />
<Skeleton class="h-10 w-full" />
</div>
<div class="space-y-2">
<Skeleton class="h-4 w-24" />
<Skeleton class="h-10 w-full" />
</div>
<Skeleton class="h-10 w-full rounded-md mt-4" />
</CardContent>
</Card>
</div>
}.into_any()
} else {
// Dashboard Skeleton
view! {
<div class="flex h-screen bg-background">
// Sidebar skeleton
<div class="w-56 border-r border-border p-4 space-y-4">
<Skeleton class="h-8 w-3/4" />
<div class="space-y-2">
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-4/5" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-3/5" />
<Skeleton class="h-6 w-full" />
</div>
</div>
// Main content skeleton
<div class="flex-1 flex flex-col">
<div class="border-b border-border p-4 flex items-center gap-4">
<Skeleton class="h-8 w-48" />
<Skeleton class="h-8 w-64" />
<div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
</div>
<div class="flex-1 p-4 space-y-3">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-3/4" />
</div>
<div class="border-t border-border p-3">
<Skeleton class="h-5 w-96" />
</div>
<Show when=move || !is_loading.0.get() fallback=|| {
// Standard 1: Always show Dashboard Skeleton
view! {
<div class="flex h-screen bg-background text-foreground overflow-hidden">
// Sidebar skeleton
<div class="w-56 border-r border-border p-4 space-y-4">
<Skeleton class="h-8 w-3/4" />
<div class="space-y-2">
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-4/5" />
<Skeleton class="h-6 w-full" />
<Skeleton class="h-6 w-3/5" />
<Skeleton class="h-6 w-full" />
</div>
</div>
}.into_any()
}
// Main content skeleton
<div class="flex-1 flex flex-col min-w-0">
<div class="border-b border-border p-4 flex items-center gap-4">
<Skeleton class="h-8 w-48" />
<Skeleton class="h-8 w-64" />
<div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
</div>
<div class="flex-1 p-4 space-y-3">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-3/4" />
</div>
<div class="border-t border-border p-3">
<Skeleton class="h-5 w-96" />
</div>
</div>
</div>
}.into_any()
}>
<Show when=move || is_authenticated.0.get() fallback=|| ()>
<Protected>

View File

@@ -1,14 +1,13 @@
use leptos::prelude::*;
use icons::PanelLeft;
use crate::components::torrent::add_torrent::AddTorrentDialog;
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
use icons::{PanelLeft, Plus};
use crate::components::torrent::add_torrent::AddTorrentDialogContent;
use crate::components::ui::button::{ButtonVariant, ButtonSize};
use crate::components::ui::sheet::{Sheet, SheetContent, SheetTrigger, SheetDirection};
use crate::components::ui::dialog::{Dialog, DialogContent, DialogTrigger};
use crate::components::layout::sidebar::Sidebar;
#[component]
pub fn Toolbar() -> impl IntoView {
let show_add_modal = signal(false);
view! {
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
// Sol kısım: Menü butonu (Mobil) + Add Torrent
@@ -33,25 +32,24 @@ pub fn Toolbar() -> impl IntoView {
</Sheet>
</div>
<Button
on:click=move |_| show_add_modal.1.set(true)
class="gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<span class="hidden sm:inline">"Add Torrent"</span>
<span class="sm:hidden">"Add"</span>
</Button>
<Dialog>
<DialogTrigger
variant=ButtonVariant::Default
class="gap-2"
>
<Plus class="w-4 h-4 md:w-5 md:h-5" />
<span class="hidden sm:inline">"Add Torrent"</span>
<span class="sm:hidden">"Add"</span>
</DialogTrigger>
<DialogContent id="add-torrent-dialog" class="sm:max-w-[425px]">
<AddTorrentDialogContent />
</DialogContent>
</Dialog>
</div>
// Sağ kısım boş
<div class="flex flex-1 items-center justify-end gap-2">
</div>
<Show when=move || show_add_modal.0.get()>
<AddTorrentDialog on_close=Callback::new(move |()| show_add_modal.1.set(false)) />
</Show>
</div>
}
}
}

View File

@@ -1,17 +1,15 @@
use leptos::prelude::*;
use leptos::task::spawn_local;
use wasm_bindgen::JsCast;
use crate::components::ui::input::{Input, InputType};
use crate::store::TorrentStore;
use crate::api;
use crate::components::ui::button::{Button, ButtonVariant};
use crate::components::ui::button::Button;
use crate::components::ui::dialog::{
DialogBody, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose
};
#[component]
pub fn AddTorrentDialog(
on_close: Callback<()>,
) -> impl IntoView {
let _store = use_context::<TorrentStore>().expect("TorrentStore not provided");
pub fn AddTorrentDialogContent() -> impl IntoView {
let uri = RwSignal::new(String::new());
let is_loading = signal(false);
let error_msg = signal(Option::<String>::None);
@@ -21,20 +19,30 @@ pub fn AddTorrentDialog(
let uri_val = uri.get();
if uri_val.is_empty() {
error_msg.1.set(Some("Please enter a Magnet URI or URL".to_string()));
error_msg.1.set(Some("Lütfen bir Magnet URI veya URL girin".to_string()));
return;
}
is_loading.1.set(true);
error_msg.1.set(None);
let on_close = on_close.clone();
spawn_local(async move {
match api::torrent::add(&uri_val).await {
Ok(_) => {
log::info!("Torrent added successfully");
crate::store::toast_success("Torrent başarıyla eklendi");
on_close.run(());
// Programmatically close the dialog by triggering the close button
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
if let Some(el) = doc.get_element_by_id("add-torrent-dialog") {
if let Some(close_btn) = el.query_selector("[data-dialog-close]").ok().flatten() {
let _ = close_btn.dyn_into::<web_sys::HtmlElement>().map(|btn| btn.click());
}
}
}
uri.set(String::new());
is_loading.1.set(false);
}
Err(e) => {
log::error!("Failed to add torrent: {:?}", e);
@@ -45,29 +53,16 @@ pub fn AddTorrentDialog(
});
};
let handle_backdrop = {
let on_close = on_close.clone();
move |e: web_sys::MouseEvent| {
e.stop_propagation();
on_close.run(());
}
};
view! {
// Backdrop overlay
<div
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
on:click=handle_backdrop
/>
// Dialog panel
<div class="fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-card p-6 shadow-lg rounded-lg sm:max-w-[425px]">
// Header
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
<h2 class="text-lg font-semibold leading-none tracking-tight">"Add Torrent"</h2>
<p class="text-sm text-muted-foreground">"Enter a Magnet link or a .torrent file URL."</p>
</div>
<DialogBody>
<DialogHeader>
<DialogTitle>"Add Torrent"</DialogTitle>
<DialogDescription>
"Enter a Magnet link or a .torrent file URL."
</DialogDescription>
</DialogHeader>
<form on:submit=handle_submit class="space-y-4">
<form on:submit=handle_submit class="space-y-4 pt-4">
<Input
r#type=InputType::Text
placeholder="magnet:?xt=urn:btih:..."
@@ -81,14 +76,10 @@ pub fn AddTorrentDialog(
</div>
})}
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
<Button
variant=ButtonVariant::Ghost
attr:r#type="button"
on:click=move |_| on_close.run(())
>
<DialogFooter class="pt-2">
<DialogClose>
"Cancel"
</Button>
</DialogClose>
<Button
attr:r#type="submit"
attr:disabled=move || is_loading.0.get()
@@ -102,21 +93,8 @@ pub fn AddTorrentDialog(
leptos::either::Either::Right(view! { "Add" })
}}
</Button>
</div>
</DialogFooter>
</form>
// Close button (X)
<Button
variant=ButtonVariant::Ghost
class="absolute right-2 top-2 size-8 p-0 opacity-70 hover:opacity-100"
on:click=move |_| on_close.run(())
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
<span class="sr-only">"Close"</span>
</Button>
</div>
</DialogBody>
}
}
}

View File

@@ -1,5 +1,6 @@
use leptos::prelude::*;
use leptos::task::spawn_local;
use wasm_bindgen::JsCast;
use std::collections::HashSet;
use icons::{ArrowUpDown, Inbox, Settings2, Play, Square, Trash2, Ellipsis, ArrowUp, ArrowDown, Check, ListFilter};
use crate::store::{get_action_messages, show_toast};
@@ -14,7 +15,17 @@ use crate::components::ui::empty::*;
use crate::components::ui::input::Input;
use crate::components::ui::multi_select::*;
use crate::components::ui::dropdown_menu::*;
use crate::components::ui::alert_dialog::*;
use crate::components::ui::alert_dialog::{
AlertDialog,
AlertDialogBody,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
};
use tailwind_fuse::tw_merge;
const ALL_COLUMNS: [(&str, &str); 8] = [
@@ -220,66 +231,78 @@ pub fn TorrentTable() -> impl IntoView {
<div class="flex items-center gap-2">
<Show when=move || has_selection.get()>
<DropdownMenu>
<DropdownMenuTrigger class="w-[140px] h-9 gap-2">
<Ellipsis class="size-4" />
{move || format!("Toplu İşlem ({})", selected_count.get())}
</DropdownMenuTrigger>
<DropdownMenuContent class="w-48">
<DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel>
<DropdownMenuGroup class="mt-2">
<DropdownMenuItem on:click=move |_| bulk_action("start")>
<Play class="mr-2 size-4" /> "Başlat"
</DropdownMenuItem>
<DropdownMenuItem on:click=move |_| bulk_action("stop")>
<Square class="mr-2 size-4" /> "Durdur"
</DropdownMenuItem>
<div class="my-1 h-px bg-border" />
<AlertDialog>
<AlertDialogTrigger class="w-full text-left">
<div class="inline-flex gap-2 items-center w-full rounded-sm px-2 py-1.5 text-sm transition-colors text-destructive hover:bg-destructive/10 focus:bg-destructive/10">
<Trash2 class="size-4" /> "Toplu Sil..."
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="text-destructive flex items-center gap-2">
<Trash2 class="size-5" />
"Toplu Silme Onayı"
</AlertDialogTitle>
<AlertDialogDescription class="pt-2">
{move || format!("Seçili {} adet torrent silinecek. Lütfen silme yöntemini seçin:", selected_count.get())}
<div class="mt-4 p-3 bg-muted/50 rounded-md text-xs border border-border italic">
"Dikkat: Verilerle birlikte silme işlemi dosyaları diskten de kalıcı olarak kaldıracaktır."
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter class="gap-2 sm:gap-0">
<div class="flex flex-col sm:flex-row gap-2 w-full justify-end">
<AlertDialogClose class="order-3 sm:order-1">"Vazgeç"</AlertDialogClose>
<div class="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger class="w-[140px] h-9 gap-2">
<Ellipsis class="size-4" />
{move || format!("Toplu İşlem ({})", selected_count.get())}
</DropdownMenuTrigger>
<DropdownMenuContent class="w-48">
<DropdownMenuLabel>"Seçili Torrentler"</DropdownMenuLabel>
<DropdownMenuGroup class="mt-2">
<DropdownMenuItem on:click=move |_| bulk_action("start")>
<Play class="mr-2 size-4" /> "Başlat"
</DropdownMenuItem>
<DropdownMenuItem on:click=move |_| bulk_action("stop")>
<Square class="mr-2 size-4" /> "Durdur"
</DropdownMenuItem>
<div class="my-1 h-px bg-border" />
// Trigger the hidden AlertDialog from this menu item
<DropdownMenuItem class="text-destructive focus:bg-destructive/10 cursor-pointer" on:click=move |_| {
if let Some(trigger) = document().get_element_by_id("bulk-delete-trigger") {
let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el: web_sys::HtmlElement| el.click());
}
}>
<Trash2 class="mr-2 size-4" /> "Toplu Sil..."
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
// Hidden AlertDialog moved outside the DropdownMenuContent to ensure proper centering
<AlertDialog>
<AlertDialogTrigger attr:id="bulk-delete-trigger" class="hidden">""</AlertDialogTrigger>
<AlertDialogContent class="sm:max-w-[425px]">
<AlertDialogBody>
<AlertDialogHeader class="space-y-3">
<AlertDialogTitle class="text-destructive flex items-center gap-2 text-xl">
<Trash2 class="size-6" />
"Toplu Silme Onayı"
</AlertDialogTitle>
<AlertDialogDescription class="text-sm leading-relaxed text-left">
{move || format!("Seçili {} adet torrent silinecek. Lütfen silme yöntemini seçin:", selected_count.get())}
<div class="mt-4 p-4 bg-destructive/5 rounded-lg border border-destructive/10 text-xs text-destructive/80 font-medium">
"⚠️ Dikkat: Verilerle birlikte silme işlemi dosyaları diskten de kalıcı olarak kaldıracaktır."
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter class="mt-6">
<div class="flex flex-col-reverse sm:flex-row gap-3 w-full sm:justify-end">
<AlertDialogClose class="sm:flex-1 md:flex-none">"Vazgeç"</AlertDialogClose>
<div class="flex flex-col sm:flex-row gap-2">
<Button
variant=ButtonVariant::Outline
class="order-2 text-foreground"
variant=ButtonVariant::Secondary
class="w-full sm:w-auto font-medium"
on:click=move |_| bulk_action("delete")
>
"Sadece Listeden Sil"
"Sadece Sil"
</Button>
<Button
variant=ButtonVariant::Destructive
class="order-1"
class="w-full sm:w-auto font-bold"
on:click=move |_| bulk_action("delete_with_data")
>
"Verilerle Birlikte Sil"
"Verilerle Sil"
</Button>
</div>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</AlertDialogFooter>
</AlertDialogBody>
</AlertDialogContent>
</AlertDialog>
</div>
</Show>
// Mobile Sort Menu

View File

@@ -72,13 +72,13 @@ pub fn DialogTrigger(
pub fn DialogContent(
children: Children,
#[prop(optional, into)] class: String,
#[prop(optional, into)] id: Option<String>,
#[prop(into, optional)] hide_close_button: Option<bool>,
#[prop(default = true)] close_on_backdrop_click: bool,
#[prop(default = "Dialog")] data_name_prefix: &'static str,
) -> impl IntoView {
let ctx = expect_context::<DialogContext>();
let merged_class = tw_merge!(
// "flex flex-col gap-4", // TODO 🐛 Bug when I try to have this.. Using DialogBody instead.
"relative bg-background border rounded-2xl shadow-lg p-6 w-full max-w-[calc(100%-2rem)] max-h-[85vh] fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-100 transition-all duration-200 data-[state=closed]:opacity-0 data-[state=closed]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100",
class
);
@@ -88,10 +88,14 @@ pub fn DialogContent(
let target_id_clone = ctx.target_id.clone();
let backdrop_id = format!("{}_backdrop", ctx.target_id);
let target_id_for_script = ctx.target_id.clone();
let backdrop_id_for_script = backdrop_id.clone();
let backdrop_behavior = if close_on_backdrop_click { "auto" } else { "manual" };
// Use provided id or fallback to random target_id
let final_id = id.unwrap_or_else(|| ctx.target_id.clone());
let final_id_for_script = final_id.clone();
let trigger_id_for_script = ctx.target_id.clone();
view! {
<script src="/lock_scroll.js"></script>
@@ -105,7 +109,7 @@ pub fn DialogContent(
<div
data-name=content_data_name
class=merged_class
id=ctx.target_id
id=final_id
data-target="target__dialog"
data-state="closed"
data-backdrop=backdrop_behavior
@@ -147,9 +151,7 @@ pub fn DialogContent(
dialog.setAttribute('data-initialized', 'true');
const openDialog = () => {{
// Lock scrolling
window.ScrollLock.lock();
if (window.ScrollLock) window.ScrollLock.lock();
dialog.setAttribute('data-state', 'open');
backdrop.setAttribute('data-state', 'open');
dialog.style.pointerEvents = 'auto';
@@ -161,28 +163,18 @@ pub fn DialogContent(
backdrop.setAttribute('data-state', 'closed');
dialog.style.pointerEvents = 'none';
backdrop.style.pointerEvents = 'none';
// Unlock scrolling after animation
window.ScrollLock.unlock(200);
}};
// Open dialog when trigger is clicked
trigger.addEventListener('click', openDialog);
// Close buttons
const closeButtons = dialog.querySelectorAll('[data-dialog-close]');
closeButtons.forEach(btn => {{
dialog.querySelectorAll('[data-dialog-close]').forEach(btn => {{
btn.addEventListener('click', closeDialog);
}});
// Close on backdrop click (if data-backdrop="auto")
backdrop.addEventListener('click', () => {{
if (dialog.getAttribute('data-backdrop') === 'auto') {{
closeDialog();
}}
}});
// Handle ESC key to close
document.addEventListener('keydown', (e) => {{
if (e.key === 'Escape' && dialog.getAttribute('data-state') === 'open') {{
e.preventDefault();
@@ -190,17 +182,12 @@ pub fn DialogContent(
}}
}});
}};
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', setupDialog);
}} else {{
setupDialog();
}}
setupDialog();
}})();
"#,
target_id_for_script,
final_id_for_script,
backdrop_id_for_script,
target_id_for_script,
trigger_id_for_script,
)}
</script>
}

View File

@@ -170,9 +170,10 @@ pub fn Toaster(#[prop(default = SonnerPosition::default())] position: SonnerPosi
<div
class=tw_merge!(
"fixed z-[100] flex gap-3 pointer-events-none w-full sm:w-[400px]",
"left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 px-4 sm:px-0", // Mobile centering fix
if is_bottom { "flex-col-reverse" } else { "flex-col" },
container_class,
"pb-[env(safe-area-inset-bottom)] pt-[env(safe-area-inset-top)] px-4 sm:px-0"
"pb-[env(safe-area-inset-bottom)] pt-[env(safe-area-inset-top)]"
)
>
<For

View File

@@ -88,25 +88,7 @@ self.addEventListener("fetch", (event) => {
return;
}
// Special strategy for WASM and Main JS to prevent Preload warnings
if (url.pathname.endsWith(".wasm") || (url.pathname.endsWith(".js") && url.pathname.includes("frontend-"))) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
return caches.match(event.request);
}),
);
return;
}
// Cache-first strategy for other static assets (CSS, Images, etc.)
// Cache-first strategy for static assets (JS, CSS, Images)
event.respondWith(
caches.match(event.request).then((response) => {
return (