From de0cd6d34ffc7ddbb98e7832159186cfed5fa686 Mon Sep 17 00:00:00 2001 From: FliesentischKing Date: Sun, 4 Oct 2026 03:06:38 +0200 Subject: [PATCH] Mass-select grid and bulk delete for moderators - Select mode in the filter row (upload.delete permission): tiles toggle selection in the capture phase, a floating action bar offers select-all and delete with confirmation popover - New POST /api/uploads/bulk-delete endpoint (validated, deduped, max 200 ids) removing rows, files and attachment data - upload-deleted broadcast on bulk and single delete; other clients refresh the grid and drop matching notifications - Also: skeleton tile grid as loading state, dropzone logic moved to the layout-level GlobalDropZone, obsolete ytimg preconnect removed Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/components/UploadTile.vue | 25 +++- app/composables/useNotifications.ts | 9 +- app/pages/index.vue | 155 ++++++++++++++++--------- app/plugins/notifications.client.ts | 11 +- server/api/uploads/bulk-delete.post.ts | 49 ++++++++ 5 files changed, 188 insertions(+), 61 deletions(-) create mode 100644 server/api/uploads/bulk-delete.post.ts diff --git a/app/components/UploadTile.vue b/app/components/UploadTile.vue index a1a869b..27c01e1 100644 --- a/app/components/UploadTile.vue +++ b/app/components/UploadTile.vue @@ -14,7 +14,11 @@ interface TileUpload { commentCount?: number } -const props = defineProps<{ upload: TileUpload }>() +const props = withDefaults(defineProps<{ upload: TileUpload, selectable?: boolean, selected?: boolean }>(), { + selectable: false, + selected: false, +}) +const emit = defineEmits<{ select: [] }>() const { t } = useI18n() const { unreadUploadIds } = useNotifications() @@ -47,7 +51,17 @@ const spanClass = computed(() => { // 1. Ladeindikator oben sofort starten // 2. Thread-Daten + Bild/Video vorladen (min. 600 ms, damit der Indikator sichtbar läuft) // 3. Wenn geladen: viewTransitionName setzen und via startViewTransition navigieren +// Capture-Phase: läuft vor NuxtLinks eigenem Klick-Handler — preventDefault + +// stopImmediatePropagation verhindert die Navigation sicher in allen Browsern +function captureClick(e: MouseEvent) { + if (!props.selectable) return + e.preventDefault() + e.stopImmediatePropagation() + emit('select') +} + async function openUpload(e: MouseEvent) { + if (props.selectable) return if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return e.preventDefault() if (navigating.value) return @@ -116,10 +130,11 @@ async function openUpload(e: MouseEvent) { :to="`/thread/${upload.id}`" class="upload-tile group block" :class="[spanClass, navigating ? 'opacity-80' : '']" + @click.capture="captureClick" @click="openUpload" >
+ + + uploadIds.includes(n.uploadId)) + if (!removed.length) return + notifications.value = notifications.value.filter(n => !uploadIds.includes(n.uploadId)) + unreadCount.value = Math.max(0, unreadCount.value - removed.filter(n => !n.isRead).length) + } + function reset() { notifications.value = [] unreadCount.value = 0 } - return { notifications, unreadCount, unreadUploadIds, fetchAll, addLive, markAllRead, markRead, reset } + return { notifications, unreadCount, unreadUploadIds, fetchAll, addLive, markAllRead, markRead, removeByUploads, reset } } diff --git a/app/pages/index.vue b/app/pages/index.vue index 34f2e50..446e898 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,9 +1,10 @@