forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<script setup lang="ts">
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ListedUser {
|
||||
id: number
|
||||
name: string
|
||||
color: string | null
|
||||
avatarUrl: string | null
|
||||
stats?: { uploads: number }
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: 'image', label: 'Bilder', icon: 'ph:image-fill' },
|
||||
{ value: 'video', label: 'Videos', icon: 'ph:video-camera-fill' },
|
||||
{ value: 'audio', label: 'Audio', icon: 'ph:music-note-fill' },
|
||||
{ value: 'youtube', label: 'YouTube', icon: 'simple-icons:youtube' },
|
||||
{ value: 'twitch', label: 'Twitch', icon: 'simple-icons:twitch' },
|
||||
{ value: 'link', label: 'Links', icon: 'ph:link-fill' },
|
||||
] as const
|
||||
|
||||
const DAY_OPTIONS = [
|
||||
{ value: '1', label: 'Heute' },
|
||||
{ value: '7', label: '7 Tage' },
|
||||
{ value: '30', label: '30 Tage' },
|
||||
{ value: '365', label: 'Letztes Jahr' },
|
||||
] as const
|
||||
|
||||
const open = ref(false)
|
||||
const uploaderOpen = ref(false)
|
||||
const users = ref<ListedUser[]>([])
|
||||
const usersLoaded = ref(false)
|
||||
|
||||
async function loadUsers() {
|
||||
if (usersLoaded.value) return
|
||||
usersLoaded.value = true
|
||||
try {
|
||||
users.value = await $fetch<ListedUser[]>('/api/users')
|
||||
}
|
||||
catch {
|
||||
users.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(open, v => v && loadUsers())
|
||||
onMounted(() => {
|
||||
if (route.query.uploader) loadUsers()
|
||||
})
|
||||
|
||||
// Für die Uploader-Auswahl nur Nutzer mit Uploads anzeigen
|
||||
const uploaders = computed(() => users.value.filter(u => !u.stats || u.stats.uploads > 0))
|
||||
|
||||
const tagFilter = computed(() => typeof route.query.tag === 'string' ? route.query.tag : undefined)
|
||||
const selectedTypes = computed<string[]>({
|
||||
get: () => typeof route.query.type === 'string' ? route.query.type.split(',').filter(Boolean) : [],
|
||||
set: v => setQuery({ type: v.length ? v.join(',') : undefined }),
|
||||
})
|
||||
const uploaderId = computed(() => typeof route.query.uploader === 'string' ? route.query.uploader : '')
|
||||
const selectedUser = computed(() => users.value.find(u => String(u.id) === uploaderId.value))
|
||||
const daysModel = computed<string>({
|
||||
get: () => typeof route.query.days === 'string' && route.query.days ? route.query.days : 'all',
|
||||
set: v => setQuery({ days: v === 'all' ? undefined : v }),
|
||||
})
|
||||
const daysOption = computed(() => DAY_OPTIONS.find(d => d.value === daysModel.value))
|
||||
|
||||
const typeLabel = (value: string) => TYPE_OPTIONS.find(t => t.value === value)?.label ?? value
|
||||
const typeIcon = (value: string) => TYPE_OPTIONS.find(t => t.value === value)?.icon ?? 'ph:image-fill'
|
||||
|
||||
const activeCount = computed(() =>
|
||||
(tagFilter.value ? 1 : 0) + selectedTypes.value.length + (uploaderId.value ? 1 : 0) + (daysOption.value ? 1 : 0),
|
||||
)
|
||||
|
||||
function setQuery(patch: Record<string, string | undefined>) {
|
||||
const query = { ...route.query }
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (value) query[key] = value
|
||||
else delete query[key]
|
||||
}
|
||||
router.replace({ query })
|
||||
}
|
||||
|
||||
function selectUser(u: ListedUser | null) {
|
||||
setQuery({ uploader: u ? String(u.id) : undefined })
|
||||
uploaderOpen.value = false
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
setQuery({ tag: undefined, type: undefined, uploader: undefined, days: undefined })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="gap-1.5">
|
||||
<Icon name="ph:sliders-horizontal-fill" class="size-4" />
|
||||
Filter
|
||||
<Badge v-if="activeCount" variant="secondary" class="ml-0.5 px-1.5 text-xs">{{ activeCount }}</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-72" align="start">
|
||||
<FieldGroup class="gap-4">
|
||||
<Field>
|
||||
<FieldLabel>Typ</FieldLabel>
|
||||
<ToggleGroup v-model="selectedTypes" type="multiple" variant="outline" :spacing="2" class="flex-wrap justify-start">
|
||||
<ToggleGroupItem
|
||||
v-for="t in TYPE_OPTIONS"
|
||||
:key="t.value"
|
||||
:value="t.value"
|
||||
:aria-label="t.label"
|
||||
class="gap-1.5 rounded-md px-2.5"
|
||||
>
|
||||
<Icon :name="t.icon" class="size-4" />
|
||||
{{ t.label }}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Uploader</FieldLabel>
|
||||
<Popover v-model:open="uploaderOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" role="combobox" :aria-expanded="uploaderOpen" class="w-full justify-between font-normal">
|
||||
<span class="flex items-center gap-1.5 truncate">
|
||||
<Icon name="ph:user-fill" class="size-4 shrink-0 text-muted-foreground" />
|
||||
<span :style="selectedUser?.color ? { color: selectedUser.color } : undefined" class="truncate">
|
||||
{{ selectedUser?.name ?? 'Alle Uploader' }}
|
||||
</span>
|
||||
</span>
|
||||
<Icon name="ph:caret-up-down-fill" class="size-4 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Nutzer suchen…" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Kein Nutzer gefunden.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem value="alle" class="gap-2" @select="selectUser(null)">
|
||||
<Icon name="ph:check-fill" :class="cn('size-4', !uploaderId ? 'opacity-100' : 'opacity-0')" />
|
||||
Alle Uploader
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
v-for="u in uploaders"
|
||||
:key="u.id"
|
||||
:value="u.name"
|
||||
class="gap-2"
|
||||
@select="selectUser(u)"
|
||||
>
|
||||
<Icon name="ph:check-fill" :class="cn('size-4', String(u.id) === uploaderId ? 'opacity-100' : 'opacity-0')" />
|
||||
<NuxtImg
|
||||
v-if="u.avatarUrl"
|
||||
:src="u.avatarUrl"
|
||||
:alt="u.name"
|
||||
width="20"
|
||||
height="20"
|
||||
fit="cover"
|
||||
class="size-5 rounded-sm"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="flex size-5 items-center justify-center rounded-sm text-[10px] font-medium"
|
||||
:style="u.color ? { backgroundColor: u.color, color: '#fff' } : { backgroundColor: 'var(--color-muted)' }"
|
||||
>{{ u.name.slice(0, 2).toUpperCase() }}</span>
|
||||
<span :style="u.color ? { color: u.color } : undefined">{{ u.name }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Zeitraum</FieldLabel>
|
||||
<Select v-model="daysModel">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Alle" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Gesamter Zeitraum</SelectItem>
|
||||
<SelectItem v-for="d in DAY_OPTIONS" :key="d.value" :value="d.value">{{ d.label }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Button variant="ghost" size="sm" :disabled="!activeCount" class="self-start" @click="clearFilters">
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<span v-if="tagFilter" class="inline-flex items-center gap-1.5 rounded-sm bg-muted px-2.5 py-1 text-sm">
|
||||
<Icon name="ph:tag-fill" class="size-3.5" />
|
||||
{{ tagFilter }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Tag-Filter entfernen"
|
||||
@click="setQuery({ tag: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-for="t in selectedTypes" :key="t" class="inline-flex items-center gap-1.5 rounded-sm bg-muted px-2.5 py-1 text-sm">
|
||||
<Icon :name="typeIcon(t)" class="size-3.5" />
|
||||
{{ typeLabel(t) }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
:aria-label="`Typ-Filter ${typeLabel(t)} entfernen`"
|
||||
@click="selectedTypes = selectedTypes.filter(v => v !== t)"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="uploaderId" class="inline-flex items-center gap-1.5 rounded-sm bg-muted px-2.5 py-1 text-sm">
|
||||
<Icon name="ph:user-fill" class="size-3.5" />
|
||||
{{ selectedUser?.name ?? '…' }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Uploader-Filter entfernen"
|
||||
@click="setQuery({ uploader: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="daysOption" class="inline-flex items-center gap-1.5 rounded-sm bg-muted px-2.5 py-1 text-sm">
|
||||
<Icon name="ph:calendar-fill" class="size-3.5" />
|
||||
{{ daysOption.label }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Zeitraum-Filter entfernen"
|
||||
@click="setQuery({ days: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user