forked from BRETT/BRETT
i18n implemented
This commit is contained in:
@@ -77,7 +77,7 @@ function formatTime(sec: number) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
|
||||
:aria-label="playing ? 'Pause' : 'Abspielen'"
|
||||
:aria-label="playing ? $t('audio.pause') : $t('audio.play')"
|
||||
@click="toggle"
|
||||
>
|
||||
<Icon name="ph:pause-fill" v-if="playing" class="size-4" />
|
||||
@@ -90,7 +90,7 @@ function formatTime(sec: number) {
|
||||
:max="Number.isFinite(duration) ? duration : 0"
|
||||
step="0.1"
|
||||
:value="currentTime"
|
||||
aria-label="Wiedergabeposition"
|
||||
:aria-label="$t('audio.seek')"
|
||||
class="min-w-0 flex-1 cursor-pointer accent-primary"
|
||||
@input="seek"
|
||||
>
|
||||
@@ -101,7 +101,7 @@ function formatTime(sec: number) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
|
||||
:aria-label="muted ? 'Ton an' : 'Stumm'"
|
||||
:aria-label="muted ? $t('audio.unmute') : $t('audio.mute')"
|
||||
@click="muted = !muted"
|
||||
>
|
||||
<Icon name="ph:speaker-x-fill" v-if="muted" class="size-4" />
|
||||
@@ -113,7 +113,7 @@ function formatTime(sec: number) {
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
aria-label="Lautstärke"
|
||||
:aria-label="$t('audio.volume')"
|
||||
class="w-16 min-w-0 cursor-pointer accent-primary sm:w-20"
|
||||
:class="muted && 'opacity-40'"
|
||||
>
|
||||
@@ -121,7 +121,7 @@ function formatTime(sec: number) {
|
||||
<Button variant="outline" size="sm" class="shrink-0" as-child>
|
||||
<a :href="src" :download="name">
|
||||
<Icon name="ph:download-fill" class="size-4" />
|
||||
Herunterladen
|
||||
{{ $t('audio.download') }}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -30,44 +30,44 @@ async function go(to: string) {
|
||||
<template>
|
||||
<CommandDialog
|
||||
v-model:open="open"
|
||||
title="Schnellsuche"
|
||||
description="Seiten, Tags oder Uploader suchen"
|
||||
:title="$t('palette.title')"
|
||||
:description="$t('palette.desc')"
|
||||
class="rounded-md!"
|
||||
>
|
||||
<CommandInput placeholder="Seiten, Tags oder Uploader suchen…" />
|
||||
<CommandInput :placeholder="$t('palette.placeholder')" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Keine Ergebnisse.</CommandEmpty>
|
||||
<CommandEmpty>{{ $t('palette.empty') }}</CommandEmpty>
|
||||
|
||||
<CommandGroup heading="Seiten">
|
||||
<CommandItem value="Startseite" class="gap-2" @select="go('/')">
|
||||
<CommandGroup :heading="$t('palette.pages')">
|
||||
<CommandItem :value="$t('palette.home')" class="gap-2" @select="go('/')">
|
||||
<Icon name="ph:house-fill" class="size-4 text-muted-foreground" />
|
||||
Startseite
|
||||
{{ $t('palette.home') }}
|
||||
<CommandShortcut>/</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem value="Benutzer" class="gap-2" @select="go('/users')">
|
||||
<CommandItem :value="$t('palette.users')" class="gap-2" @select="go('/users')">
|
||||
<Icon name="ph:users-fill" class="size-4 text-muted-foreground" />
|
||||
Benutzer
|
||||
{{ $t('palette.users') }}
|
||||
<CommandShortcut>/users</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem v-if="user" value="Mein Profil" class="gap-2" @select="go(`/user/${user.id}`)">
|
||||
<CommandItem v-if="user" :value="$t('palette.myProfile')" class="gap-2" @select="go(`/user/${user.id}`)">
|
||||
<Icon name="ph:user-fill" class="size-4 text-muted-foreground" />
|
||||
Mein Profil
|
||||
{{ $t('palette.myProfile') }}
|
||||
</CommandItem>
|
||||
<CommandItem v-if="user" value="Einstellungen" class="gap-2" @select="go('/settings')">
|
||||
<CommandItem v-if="user" :value="$t('palette.settings')" class="gap-2" @select="go('/settings')">
|
||||
<Icon name="ph:gear-fill" class="size-4 text-muted-foreground" />
|
||||
Einstellungen
|
||||
{{ $t('palette.settings') }}
|
||||
</CommandItem>
|
||||
<CommandItem v-if="can('admin.access')" value="Admin-Bereich" class="gap-2" @select="go('/admin')">
|
||||
<CommandItem v-if="can('admin.access')" :value="$t('palette.adminArea')" class="gap-2" @select="go('/admin')">
|
||||
<Icon name="ph:shield-fill" class="size-4 text-muted-foreground" />
|
||||
Admin-Bereich
|
||||
{{ $t('palette.adminArea') }}
|
||||
</CommandItem>
|
||||
<CommandItem v-if="!user" value="Login" class="gap-2" @select="go('/login')">
|
||||
<CommandItem v-if="!user" :value="$t('palette.login')" class="gap-2" @select="go('/login')">
|
||||
<Icon name="ph:sign-in-fill" class="size-4 text-muted-foreground" />
|
||||
Login
|
||||
{{ $t('palette.login') }}
|
||||
</CommandItem>
|
||||
<CommandItem v-if="!user" value="Registrieren" class="gap-2" @select="go('/register')">
|
||||
<CommandItem v-if="!user" :value="$t('palette.register')" class="gap-2" @select="go('/register')">
|
||||
<Icon name="ph:user-plus-fill" class="size-4 text-muted-foreground" />
|
||||
Registrieren
|
||||
{{ $t('palette.register') }}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommandGroup v-if="tags.length" heading="Tags">
|
||||
<CommandGroup v-if="tags.length" :heading="$t('palette.tags')">
|
||||
<CommandItem
|
||||
v-for="t in tags"
|
||||
:key="t.name"
|
||||
@@ -50,7 +50,7 @@ onMounted(async () => {
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandGroup heading="Uploader">
|
||||
<CommandGroup :heading="$t('palette.uploaders')">
|
||||
<CommandItem
|
||||
v-for="u in users"
|
||||
:key="u.id"
|
||||
|
||||
@@ -53,7 +53,7 @@ function fileIcon(a: CommentAttachment) {
|
||||
</div>
|
||||
<template v-if="hasFileAttachments">
|
||||
<div class="mb-1.5 flex items-center gap-2">
|
||||
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Anhänge</span>
|
||||
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">{{ $t('comments.attachments') }}</span>
|
||||
<Separator class="flex-1" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
@@ -80,7 +80,7 @@ function fileIcon(a: CommentAttachment) {
|
||||
:key="a.id"
|
||||
type="button"
|
||||
class="flex max-w-48 cursor-pointer items-center gap-1.5 rounded-md border bg-muted/50 px-2 py-1 text-xs transition-colors hover:border-primary hover:bg-accent"
|
||||
:title="`${a.name} abspielen`"
|
||||
:title="$t('comments.playAttachment', { name: a.name })"
|
||||
@click.stop="audioAtt = a"
|
||||
>
|
||||
<img v-if="a.cover" :src="a.cover" :alt="a.name" class="size-5 shrink-0 rounded-sm object-cover">
|
||||
|
||||
@@ -94,7 +94,7 @@ const parts = computed<Part[]>(() => {
|
||||
</template>
|
||||
<!-- Kompakt-Modus (Sidebar): Icon + Hostname statt Embed/Card -->
|
||||
<span
|
||||
v-else-if="compact && part.type !== 'text'"
|
||||
v-else-if="compact"
|
||||
class="mx-0.5 inline-flex items-center gap-1 rounded-sm bg-muted px-1.5 py-0.5 align-middle text-xs text-muted-foreground"
|
||||
>
|
||||
<Icon v-if="part.type === 'youtube'" name="simple-icons:youtube" class="size-3.5" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
const props = defineProps<{ commentId: number }>()
|
||||
|
||||
const { toggle } = useCommentReactions()
|
||||
const { t } = useI18n()
|
||||
|
||||
const open = ref(false)
|
||||
const busy = ref(false)
|
||||
@@ -17,7 +18,7 @@ async function pick(emoji: string) {
|
||||
await toggle(props.commentId, emoji)
|
||||
}
|
||||
catch {
|
||||
toast.error('Reaktion fehlgeschlagen')
|
||||
toast.error(t('comments.reactionFailed'))
|
||||
}
|
||||
finally {
|
||||
busy.value = false
|
||||
@@ -31,7 +32,7 @@ async function pick(emoji: string) {
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer self-center rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
aria-label="Reaktion hinzufügen"
|
||||
:aria-label="$t('comments.addReaction')"
|
||||
>
|
||||
<Icon name="ph:smiley-fill" class="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -7,6 +7,7 @@ const props = defineProps<{ commentId: number }>()
|
||||
|
||||
const { user } = useAuth()
|
||||
const { reactionsFor, toggle } = useCommentReactions()
|
||||
const { t } = useI18n()
|
||||
|
||||
const busy = ref(false)
|
||||
const pending = ref<string | null>(null)
|
||||
@@ -21,7 +22,7 @@ const burstNonce = ref(0)
|
||||
|
||||
async function pick(emoji: string) {
|
||||
if (!user.value) {
|
||||
toast.error('Melde dich an, um zu reagieren.')
|
||||
toast.error(t('comments.loginToReact'))
|
||||
return
|
||||
}
|
||||
if (busy.value) return
|
||||
@@ -33,7 +34,7 @@ async function pick(emoji: string) {
|
||||
await toggle(props.commentId, emoji)
|
||||
}
|
||||
catch {
|
||||
toast.error('Reaktion fehlgeschlagen')
|
||||
toast.error(t('comments.reactionFailed'))
|
||||
}
|
||||
finally {
|
||||
busy.value = false
|
||||
|
||||
@@ -42,7 +42,7 @@ const mergedComments = computed(() => {
|
||||
<Sidebar side="right" collapsible="none" class="sticky top-0 hidden h-svh shrink-0 overflow-hidden rounded-l-md border-l lg:flex">
|
||||
<SidebarContent class="px-2 py-2">
|
||||
<p v-if="!mergedComments.length" class="px-2 py-1 text-sm text-muted-foreground">
|
||||
Noch keine Kommentare.
|
||||
{{ $t('comments.none') }}
|
||||
</p>
|
||||
<NuxtLink
|
||||
v-for="comment in mergedComments"
|
||||
|
||||
@@ -90,7 +90,7 @@ const globalFilter = computed(() => String(table.atoms.globalFilter.get() ?? '')
|
||||
</TableRow>
|
||||
</template>
|
||||
<TableEmpty v-else :colspan="columns.length">
|
||||
Keine Einträge.
|
||||
{{ $t('misc.noEntries') }}
|
||||
</TableEmpty>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -98,7 +98,7 @@ const globalFilter = computed(() => String(table.atoms.globalFilter.get() ?? '')
|
||||
|
||||
<div v-if="pageCount > 1" class="flex items-center justify-between">
|
||||
<span class="text-sm text-muted-foreground">
|
||||
Seite {{ pageIndex + 1 }} von {{ pageCount }}
|
||||
{{ $t('misc.pageOf', { page: pageIndex + 1, total: pageCount }) }}
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
@@ -107,7 +107,7 @@ const globalFilter = computed(() => String(table.atoms.globalFilter.get() ?? '')
|
||||
:disabled="!table.getCanPreviousPage()"
|
||||
@click="table.previousPage()"
|
||||
>
|
||||
Zurück
|
||||
{{ $t('common.previous') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -115,7 +115,7 @@ const globalFilter = computed(() => String(table.atoms.globalFilter.get() ?? '')
|
||||
:disabled="!table.getCanNextPage()"
|
||||
@click="table.nextPage()"
|
||||
>
|
||||
Weiter
|
||||
{{ $t('common.next') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,11 @@ const props = withDefaults(defineProps<{
|
||||
hint?: string
|
||||
}>(), {
|
||||
accept: 'image/*,video/*,audio/*,.mp3,.flac',
|
||||
hint: 'Bilder, Videos & Audio, max. 200 MB',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const hintText = computed(() => props.hint ?? t('upload.dropzoneHint'))
|
||||
|
||||
const dropzoneRef = ref<HTMLElement>()
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const previewUrl = ref<string | null>(null)
|
||||
@@ -25,7 +27,7 @@ const { isOverDropZone } = useDropZone(dropzoneRef, {
|
||||
if (props.disabled) return
|
||||
const file = files?.find(isMediaFile)
|
||||
if (file) select(file)
|
||||
else if (files?.length) toast.error('Nur Bilder, Videos und Audiodateien sind erlaubt')
|
||||
else if (files?.length) toast.error(t('upload.onlyMedia'))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -94,14 +96,14 @@ onUnmounted(() => {
|
||||
@click.stop="select(null)"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-4" />
|
||||
Entfernen
|
||||
{{ $t('common.remove') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Icon name="ph:cloud-arrow-up-fill" class="size-8 text-muted-foreground" />
|
||||
<span>Datei hierher ziehen oder klicken</span>
|
||||
<span class="text-xs text-muted-foreground">{{ hint }}</span>
|
||||
<span>{{ $t('upload.dropzoneText') }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ hintText }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -74,7 +74,7 @@ onKeyStroke('Escape', () => emit('close'))
|
||||
@click="emit('close')"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-5" />
|
||||
<span class="sr-only">Schließen</span>
|
||||
<span class="sr-only">{{ $t('common.close') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -69,7 +69,7 @@ function onKeydown(event: KeyboardEvent) {
|
||||
v-model="model"
|
||||
:placeholder="props.placeholder"
|
||||
class="min-h-32 pb-9"
|
||||
aria-label="Kommentar"
|
||||
:aria-label="$t('comments.commentAria')"
|
||||
aria-autocomplete="list"
|
||||
:aria-expanded="open"
|
||||
:aria-controls="open ? listId : undefined"
|
||||
@@ -92,7 +92,7 @@ function onKeydown(event: KeyboardEvent) {
|
||||
@close-auto-focus.prevent
|
||||
@interact-outside="event => { if (event.target === textarea) event.preventDefault() }"
|
||||
>
|
||||
<div :id="listId" role="listbox" aria-label="Benutzer erwähnen">
|
||||
<div :id="listId" role="listbox" :aria-label="$t('comments.mentionAria')">
|
||||
<button
|
||||
v-for="(user, index) in suggestions"
|
||||
:id="`${listId}-${index}`"
|
||||
|
||||
@@ -21,7 +21,7 @@ async function goTo(n: AppNotification) {
|
||||
<template>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="relative" aria-label="Benachrichtigungen">
|
||||
<Button variant="ghost" size="icon" class="relative" :aria-label="$t('notifications.aria')">
|
||||
<Icon name="ph:bell-fill" class="size-5" />
|
||||
<span
|
||||
v-if="unreadCount"
|
||||
@@ -32,7 +32,7 @@ async function goTo(n: AppNotification) {
|
||||
<PopoverContent align="end" class="w-80 gap-0 p-0">
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<p v-if="!notifications.length" class="px-4 py-6 text-center text-sm text-muted-foreground">
|
||||
Keine Benachrichtigungen.
|
||||
{{ $t('notifications.empty') }}
|
||||
</p>
|
||||
<button
|
||||
v-for="n in notifications"
|
||||
@@ -69,7 +69,7 @@ async function goTo(n: AppNotification) {
|
||||
class="truncate font-medium"
|
||||
:style="n.actorColor ? { color: n.actorColor } : undefined"
|
||||
>{{ n.actorName }}</span>
|
||||
<span class="shrink-0 text-muted-foreground">{{ n.isMention ? 'hat dich erwähnt' : 'hat kommentiert' }}</span>
|
||||
<span class="shrink-0 text-muted-foreground">{{ n.isMention ? $t('notifications.mentioned') : $t('notifications.commented') }}</span>
|
||||
<span class="ml-auto shrink-0 text-xs text-muted-foreground">{{ timeAgo(n.createdAt) }}</span>
|
||||
</div>
|
||||
<p class="mt-0.5 line-clamp-2 break-words text-muted-foreground">
|
||||
|
||||
@@ -44,7 +44,7 @@ async function onLogout() {
|
||||
<Button variant="ghost" as-child>
|
||||
<NuxtLink to="/users" class="flex items-center gap-1.5">
|
||||
<Icon name="ph:users-fill" class="size-4" />
|
||||
Benutzer
|
||||
{{ $t('nav.users') }}
|
||||
</NuxtLink>
|
||||
</Button>
|
||||
<Button v-if="can('admin.access')" variant="ghost" as-child>
|
||||
@@ -56,12 +56,12 @@ async function onLogout() {
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto flex h-9 cursor-pointer items-center gap-2 rounded-md border bg-muted/40 px-3 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Schnellsuche öffnen (Strg+K)"
|
||||
:aria-label="$t('nav.searchAria')"
|
||||
@click="paletteOpen = true"
|
||||
>
|
||||
<Icon name="ph:magnifying-glass-fill" class="size-4" />
|
||||
<span class="hidden text-left sm:inline">Suchen…</span>
|
||||
<kbd class="hidden rounded-sm border bg-background px-1.5 text-xs sm:inline">Strg K</kbd>
|
||||
<span class="hidden text-left sm:inline">{{ $t('nav.search') }}</span>
|
||||
<kbd class="hidden rounded-sm border bg-background px-1.5 text-xs sm:inline">{{ $t('nav.searchShortcut') }}</kbd>
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<Tooltip>
|
||||
@@ -75,7 +75,7 @@ async function onLogout() {
|
||||
<Icon name="ph:squares-four-fill" v-else class="size-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ uniform ? 'Bento-Layout' : 'Einheitliche Größe' }}</TooltipContent>
|
||||
<TooltipContent>{{ uniform ? $t('nav.gridBento') : $t('nav.gridUniform') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<ThemeSelector />
|
||||
<template v-if="user">
|
||||
@@ -104,29 +104,29 @@ async function onLogout() {
|
||||
<DropdownMenuItem as-child>
|
||||
<NuxtLink :to="`/user/${user.id}`" class="flex items-center gap-2">
|
||||
<Icon name="ph:user-fill" class="size-4" />
|
||||
Profil
|
||||
{{ $t('nav.profile') }}
|
||||
</NuxtLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as-child>
|
||||
<NuxtLink to="/settings" class="flex items-center gap-2">
|
||||
<Icon name="ph:gear-fill" class="size-4" />
|
||||
Einstellungen
|
||||
{{ $t('nav.settings') }}
|
||||
</NuxtLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" class="flex items-center gap-2" @click="onLogout">
|
||||
<Icon name="ph:sign-out-fill" class="size-4" />
|
||||
Ausloggen
|
||||
{{ $t('auth.logout') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button variant="ghost" as-child>
|
||||
<NuxtLink to="/login">Login</NuxtLink>
|
||||
<NuxtLink to="/login">{{ $t('auth.login') }}</NuxtLink>
|
||||
</Button>
|
||||
<Button as-child>
|
||||
<NuxtLink to="/register">Registrieren</NuxtLink>
|
||||
<NuxtLink to="/register">{{ $t('auth.register') }}</NuxtLink>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
const props = defineProps<{ uploadId: string, enabled: boolean }>()
|
||||
const emit = defineEmits<{ change: [uploadId: string, enabled: boolean] }>()
|
||||
const busy = shallowRef(false)
|
||||
const { t } = useI18n()
|
||||
const label = computed(() => props.enabled
|
||||
? 'Benachrichtigungen für diesen Thread deaktivieren'
|
||||
: 'Benachrichtigungen für diesen Thread aktivieren')
|
||||
? t('thread.subscribeOff')
|
||||
: t('thread.subscribeOn'))
|
||||
|
||||
async function toggle() {
|
||||
if (busy.value) return
|
||||
@@ -20,10 +21,10 @@ async function toggle() {
|
||||
body: { enabled: !props.enabled },
|
||||
})
|
||||
emit('change', uploadId, result.enabled)
|
||||
toast.success(result.enabled ? 'Thread-Benachrichtigungen aktiviert' : 'Thread-Benachrichtigungen deaktiviert')
|
||||
toast.success(result.enabled ? t('thread.subOn') : t('thread.subOff'))
|
||||
}
|
||||
catch {
|
||||
toast.error('Abonnement konnte nicht geändert werden')
|
||||
toast.error(t('thread.subFailed'))
|
||||
}
|
||||
finally {
|
||||
busy.value = false
|
||||
|
||||
@@ -25,21 +25,22 @@ interface ListedUser {
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
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' },
|
||||
{ value: 'image', labelKey: 'filters.images', icon: 'ph:image-fill' },
|
||||
{ value: 'video', labelKey: 'filters.videos', icon: 'ph:video-camera-fill' },
|
||||
{ value: 'audio', labelKey: 'filters.audio', icon: 'ph:music-note-fill' },
|
||||
{ value: 'youtube', labelKey: null, icon: 'simple-icons:youtube' },
|
||||
{ value: 'twitch', labelKey: null, icon: 'simple-icons:twitch' },
|
||||
{ value: 'link', labelKey: 'filters.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' },
|
||||
{ value: '1', labelKey: 'filters.today' },
|
||||
{ value: '7', labelKey: 'filters.days7' },
|
||||
{ value: '30', labelKey: 'filters.days30' },
|
||||
{ value: '365', labelKey: 'filters.lastYear' },
|
||||
] as const
|
||||
|
||||
const open = ref(false)
|
||||
@@ -79,7 +80,10 @@ const daysModel = computed<string>({
|
||||
})
|
||||
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 typeLabel = (value: string) => {
|
||||
const option = TYPE_OPTIONS.find(o => o.value === value)
|
||||
return option ? (option.labelKey ? t(option.labelKey) : option.value) : value
|
||||
}
|
||||
const typeIcon = (value: string) => TYPE_OPTIONS.find(t => t.value === value)?.icon ?? 'ph:image-fill'
|
||||
|
||||
const activeCount = computed(() =>
|
||||
@@ -111,36 +115,36 @@ function clearFilters() {
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="gap-1.5">
|
||||
<Icon name="ph:sliders-horizontal-fill" class="size-4" />
|
||||
Filter
|
||||
{{ $t('filters.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>
|
||||
<FieldLabel>{{ $t('filters.type') }}</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"
|
||||
v-for="o in TYPE_OPTIONS"
|
||||
:key="o.value"
|
||||
:value="o.value"
|
||||
:aria-label="typeLabel(o.value)"
|
||||
class="gap-1.5 rounded-md px-2.5"
|
||||
>
|
||||
<Icon :name="t.icon" class="size-4" />
|
||||
{{ t.label }}
|
||||
<Icon :name="o.icon" class="size-4" />
|
||||
{{ typeLabel(o.value) }}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Uploader</FieldLabel>
|
||||
<FieldLabel>{{ $t('filters.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' }}
|
||||
{{ selectedUser?.name ?? $t('filters.allUploaders') }}
|
||||
</span>
|
||||
</span>
|
||||
<Icon name="ph:caret-up-down-fill" class="size-4 shrink-0 text-muted-foreground" />
|
||||
@@ -148,13 +152,13 @@ function clearFilters() {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Nutzer suchen…" />
|
||||
<CommandInput :placeholder="$t('filters.searchUser')" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Kein Nutzer gefunden.</CommandEmpty>
|
||||
<CommandEmpty>{{ $t('filters.noUser') }}</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
|
||||
{{ $t('filters.allUploaders') }}
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
v-for="u in uploaders"
|
||||
@@ -187,19 +191,19 @@ function clearFilters() {
|
||||
</Popover>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Zeitraum</FieldLabel>
|
||||
<FieldLabel>{{ $t('filters.period') }}</FieldLabel>
|
||||
<Select v-model="daysModel">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Alle" />
|
||||
<SelectValue :placeholder="$t('filters.allTime')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Gesamter Zeitraum</SelectItem>
|
||||
<SelectItem v-for="d in DAY_OPTIONS" :key="d.value" :value="d.value">{{ d.label }}</SelectItem>
|
||||
<SelectItem value="all">{{ $t('filters.allTime') }}</SelectItem>
|
||||
<SelectItem v-for="d in DAY_OPTIONS" :key="d.value" :value="d.value">{{ $t(d.labelKey) }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Button variant="ghost" size="sm" :disabled="!activeCount" class="self-start" @click="clearFilters">
|
||||
Zurücksetzen
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</PopoverContent>
|
||||
@@ -211,7 +215,7 @@ function clearFilters() {
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Tag-Filter entfernen"
|
||||
:aria-label="$t('filters.removeTag')"
|
||||
@click="setQuery({ tag: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
@@ -223,7 +227,7 @@ function clearFilters() {
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
:aria-label="`Typ-Filter ${typeLabel(t)} entfernen`"
|
||||
:aria-label="$t('filters.removeType', { label: typeLabel(t) })"
|
||||
@click="selectedTypes = selectedTypes.filter(v => v !== t)"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
@@ -235,7 +239,7 @@ function clearFilters() {
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Uploader-Filter entfernen"
|
||||
:aria-label="$t('filters.removeUploader')"
|
||||
@click="setQuery({ uploader: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
@@ -243,11 +247,11 @@ function clearFilters() {
|
||||
</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 }}
|
||||
{{ daysOption ? $t(daysOption.labelKey) : '' }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-label="Zeitraum-Filter entfernen"
|
||||
:aria-label="$t('filters.removePeriod')"
|
||||
@click="setQuery({ days: undefined })"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-3.5" />
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
const { user } = useAuth()
|
||||
const { t } = useI18n()
|
||||
const open = ref(false)
|
||||
const file = ref<File | null>(null)
|
||||
const url = ref('')
|
||||
@@ -43,7 +44,7 @@ async function onSubmit() {
|
||||
}
|
||||
catch (err) {
|
||||
const e = err as { data?: { statusMessage?: string, message?: string } }
|
||||
errorMessage.value = e.data?.statusMessage ?? e.data?.message ?? 'Upload fehlgeschlagen'
|
||||
errorMessage.value = e.data?.statusMessage ?? e.data?.message ?? t('upload.failed')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -56,39 +57,39 @@ async function onSubmit() {
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost">
|
||||
<Icon name="ph:cloud-arrow-up-fill" class="size-4" />
|
||||
Hochladen
|
||||
{{ $t('upload.upload') }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" class="w-160">
|
||||
<template v-if="user">
|
||||
<PopoverHeader>
|
||||
<PopoverTitle>Hochladen</PopoverTitle>
|
||||
<PopoverDescription>Datei oder URL — Bilder, Videos, Audio (MP3/FLAC), YouTube- und Twitch-Links.</PopoverDescription>
|
||||
<PopoverTitle>{{ $t('upload.upload') }}</PopoverTitle>
|
||||
<PopoverDescription>{{ $t('upload.desc') }}</PopoverDescription>
|
||||
</PopoverHeader>
|
||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||
<div class="space-y-1.5">
|
||||
<Label>Datei</Label>
|
||||
<Label>{{ $t('upload.file') }}</Label>
|
||||
<FileDropzone v-model="file" :disabled="!!url.trim()" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="upload-url">oder URL</Label>
|
||||
<Label for="upload-url">{{ $t('upload.orUrl') }}</Label>
|
||||
<Input id="upload-url" v-model="url" type="url" placeholder="https://…" :disabled="!!file" />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Direkte Bild-/Video-URLs werden gespeichert, YouTube- und Twitch-Links eingebettet.
|
||||
{{ $t('upload.urlHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="errorMessage" class="text-sm text-destructive">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<Button type="submit" class="w-full" :disabled="loading || (!file && !url.trim())">
|
||||
{{ loading ? 'Lädt hoch…' : 'Hochladen' }}
|
||||
{{ loading ? $t('upload.uploading') : $t('upload.upload') }}
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
<p v-else class="text-sm text-muted-foreground">
|
||||
<NuxtLink to="/login" class="text-primary underline" @click="open = false">
|
||||
Melde dich an
|
||||
</NuxtLink>, um Dateien hochzuladen.
|
||||
{{ $t('common.loginLink') }}
|
||||
</NuxtLink>{{ $t('upload.loginToUpload') }}
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -8,6 +8,7 @@ const emit = defineEmits<{ subscriptionChange: [uploadId: string, enabled: boole
|
||||
|
||||
const { user } = useAuth()
|
||||
const { reactionsFor, toggle } = useUploadReactions()
|
||||
const { t } = useI18n()
|
||||
|
||||
const busy = ref(false)
|
||||
const pending = ref<string | null>(null)
|
||||
@@ -22,7 +23,7 @@ const burstNonce = ref(0)
|
||||
|
||||
async function pick(emoji: string) {
|
||||
if (!user.value) {
|
||||
toast.error('Melde dich an, um zu reagieren.')
|
||||
toast.error(t('comments.loginToReact'))
|
||||
return
|
||||
}
|
||||
if (busy.value) return
|
||||
@@ -36,7 +37,7 @@ async function pick(emoji: string) {
|
||||
emit('subscriptionChange', uploadId, result.isSubscribed)
|
||||
}
|
||||
catch {
|
||||
toast.error('Reaktion fehlgeschlagen')
|
||||
toast.error(t('comments.reactionFailed'))
|
||||
}
|
||||
finally {
|
||||
busy.value = false
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { toast } from 'vue-sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TileUpload {
|
||||
id: string
|
||||
@@ -14,6 +15,7 @@ interface TileUpload {
|
||||
}
|
||||
|
||||
const props = defineProps<{ upload: TileUpload }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { unreadUploadIds } = useNotifications()
|
||||
const { uniform } = useGridMode()
|
||||
@@ -98,7 +100,7 @@ async function openUpload(e: MouseEvent) {
|
||||
catch {
|
||||
indicator.finish({ error: true })
|
||||
navigating.value = false
|
||||
toast.error('Thread konnte nicht geladen werden')
|
||||
toast.error(t('upload.threadLoadFailed'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ async function openUpload(e: MouseEvent) {
|
||||
@click="openUpload"
|
||||
>
|
||||
<div
|
||||
class="upload-tile-media relative h-full w-full overflow-hidden rounded-md"
|
||||
:class="cn('upload-tile-media relative h-full w-full overflow-hidden rounded-md', hasNotification && 'ring-4 ring-primary')"
|
||||
:style="lastViewedId === upload.id ? 'view-transition-name: upload-media' : undefined"
|
||||
>
|
||||
<NuxtImg
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { summaries, ensureSummary } = useUserSummaries()
|
||||
const { locale } = useI18n()
|
||||
const summary = computed(() => summaries.value[props.userId])
|
||||
const avatar = computed(() => summary.value?.avatarUrl ?? props.userAvatarUrl ?? null)
|
||||
const color = computed(() => summary.value?.color ?? props.userColor ?? null)
|
||||
@@ -37,7 +38,7 @@ function onOpen(open: boolean) {
|
||||
}
|
||||
|
||||
function formatDate(value: string) {
|
||||
return new Date(value).toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
return new Date(value).toLocaleDateString(locale.value, { day: '2-digit', month: 'short', year: 'numeric' })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -78,18 +79,18 @@ function formatDate(value: string) {
|
||||
</p>
|
||||
<p v-if="summary" class="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Icon name="ph:calendar-fill" class="size-3" />
|
||||
seit {{ formatDate(summary.createdAt) }}
|
||||
{{ $t('misc.since', { date: formatDate(summary.createdAt) }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="summary" class="relative mt-3 flex items-center gap-4 border-t pt-3 text-xs text-muted-foreground">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<Icon name="ph:cloud-arrow-up-fill" class="size-3.5" />
|
||||
{{ summary.stats.uploads }} Uploads
|
||||
{{ summary.stats.uploads }} {{ $t('misc.uploadsCol') }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<Icon name="ph:chat-fill" class="size-3.5" />
|
||||
{{ summary.stats.comments }} Kommentare
|
||||
{{ summary.stats.comments }} {{ $t('misc.commentsCol') }}
|
||||
</span>
|
||||
</div>
|
||||
<CardBorderEffect v-if="borderEffect" :effect="borderEffect" :color="summary?.cardBorderEffectColor ?? color ?? '#6366f1'" />
|
||||
|
||||
@@ -34,7 +34,7 @@ function formatTime(sec: number) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
|
||||
:aria-label="playing ? 'Pause' : 'Abspielen'"
|
||||
:aria-label="playing ? $t('audio.pause') : $t('audio.play')"
|
||||
@click="toggle"
|
||||
>
|
||||
<Icon name="ph:pause-fill" v-if="playing" class="size-4" />
|
||||
@@ -47,7 +47,7 @@ function formatTime(sec: number) {
|
||||
:max="Number.isFinite(duration) ? duration : 0"
|
||||
step="0.1"
|
||||
:value="currentTime"
|
||||
aria-label="Wiedergabeposition"
|
||||
:aria-label="$t('audio.seek')"
|
||||
class="min-w-0 flex-1 cursor-pointer accent-primary"
|
||||
@input="seek"
|
||||
>
|
||||
@@ -55,7 +55,7 @@ function formatTime(sec: number) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-accent"
|
||||
:aria-label="muted ? 'Ton an' : 'Stumm'"
|
||||
:aria-label="muted ? $t('audio.unmute') : $t('audio.mute')"
|
||||
@click="muted = !muted"
|
||||
>
|
||||
<Icon name="ph:speaker-x-fill" v-if="muted" class="size-3.5" />
|
||||
@@ -67,7 +67,7 @@ function formatTime(sec: number) {
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
aria-label="Lautstärke"
|
||||
:aria-label="$t('audio.volume')"
|
||||
class="w-14 min-w-0 shrink-0 cursor-pointer accent-primary"
|
||||
:class="muted && 'opacity-40'"
|
||||
>
|
||||
@@ -75,8 +75,8 @@ function formatTime(sec: number) {
|
||||
:href="src"
|
||||
:download="name"
|
||||
class="flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
aria-label="Herunterladen"
|
||||
title="Herunterladen"
|
||||
:aria-label="$t('audio.download')"
|
||||
:title="$t('audio.download')"
|
||||
>
|
||||
<Icon name="ph:download-fill" class="size-3.5" />
|
||||
</a>
|
||||
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
|
||||
<template>
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
:aria-label="$t('misc.breadcrumb')"
|
||||
data-slot="breadcrumb"
|
||||
:class="cn('', props.class)"
|
||||
>
|
||||
|
||||
@@ -18,6 +18,6 @@ const props = defineProps<{
|
||||
<slot>
|
||||
<Icon name="ph:dots-three-fill" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
<span class="sr-only">{{ $t('misc.more') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -430,7 +430,7 @@ function updateAlpha(event: MouseEvent | TouchEvent) {
|
||||
type="button"
|
||||
class="ring-offset-background h-5 w-5 shrink-0 cursor-pointer rounded ring-2 ring-transparent ring-offset-1 transition-all duration-100 hover:ring-current"
|
||||
:style="{ backgroundColor: color }"
|
||||
:aria-label="`Set color to ${color}`"
|
||||
:aria-label="$t('misc.setColor', { color })"
|
||||
@click="setColorFromHex(color)"
|
||||
@keydown.enter="setColorFromHex(color)"
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
>
|
||||
<Button variant="ghost" class="absolute top-4 right-4 bg-secondary" size="icon-sm">
|
||||
<Icon name="ph:x-fill" />
|
||||
<span class="sr-only">Close</span>
|
||||
<span class="sr-only">{{ $t('common.close') }}</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
||||
@@ -51,7 +51,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
<span class="sr-only">{{ $t('common.close') }}</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
|
||||
@@ -19,7 +19,7 @@ const delegatedProps = reactiveOmit(props, 'class')
|
||||
>
|
||||
<slot>
|
||||
<Icon name="ph:dots-three-fill" />
|
||||
<span class="sr-only">Weitere Seiten</span>
|
||||
<span class="sr-only">{{ $t('pagination.more') }}</span>
|
||||
</slot>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
|
||||
@@ -22,13 +22,13 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
<template>
|
||||
<PaginationFirst
|
||||
data-slot="pagination-first"
|
||||
aria-label="Erste Seite"
|
||||
:aria-label="$t('pagination.first')"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), '', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<Icon name="ph:caret-double-left-fill" data-icon="inline-start" />
|
||||
<span class="hidden sm:block">Erste</span>
|
||||
<span class="hidden sm:block">{{ $t('pagination.firstShort') }}</span>
|
||||
</slot>
|
||||
</PaginationFirst>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ const delegatedProps = reactiveOmit(props, 'class', 'size', 'isActive')
|
||||
<template>
|
||||
<PaginationListItem
|
||||
data-slot="pagination-item"
|
||||
:aria-label="`Seite ${value}`"
|
||||
:aria-label="$t('pagination.page', { page: value })"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
buttonVariants({
|
||||
|
||||
@@ -22,12 +22,12 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
<template>
|
||||
<PaginationLast
|
||||
data-slot="pagination-last"
|
||||
aria-label="Letzte Seite"
|
||||
:aria-label="$t('pagination.last')"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), '', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Letzte</span>
|
||||
<span class="hidden sm:block">{{ $t('pagination.lastShort') }}</span>
|
||||
<Icon name="ph:caret-double-right-fill" data-icon="inline-end" />
|
||||
</slot>
|
||||
</PaginationLast>
|
||||
|
||||
@@ -22,12 +22,12 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
<template>
|
||||
<PaginationNext
|
||||
data-slot="pagination-next"
|
||||
aria-label="Nächste Seite"
|
||||
:aria-label="$t('pagination.next')"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'pr-1.5!', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Weiter</span>
|
||||
<span class="hidden sm:block">{{ $t('common.next') }}</span>
|
||||
<Icon name="ph:caret-right-fill" data-icon="inline-end" class="cn-rtl-flip" />
|
||||
</slot>
|
||||
</PaginationNext>
|
||||
|
||||
@@ -22,13 +22,13 @@ const forwarded = useForwardProps(delegatedProps)
|
||||
<template>
|
||||
<PaginationPrev
|
||||
data-slot="pagination-previous"
|
||||
aria-label="Vorherige Seite"
|
||||
:aria-label="$t('pagination.prev')"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'pl-1.5!', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<Icon name="ph:caret-left-fill" data-icon="inline-start" class="cn-rtl-flip" />
|
||||
<span class="hidden sm:block">Zurück</span>
|
||||
<span class="hidden sm:block">{{ $t('common.previous') }}</span>
|
||||
</slot>
|
||||
</PaginationPrev>
|
||||
</template>
|
||||
|
||||
@@ -52,7 +52,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
>
|
||||
<Button variant="ghost" class="absolute top-4 right-4 bg-secondary" size="icon-sm">
|
||||
<Icon name="ph:x-fill" />
|
||||
<span class="sr-only">Close</span>
|
||||
<span class="sr-only">{{ $t('common.close') }}</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
||||
@@ -42,8 +42,8 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
}"
|
||||
>
|
||||
<SheetHeader class="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
<SheetTitle>{{ $t('misc.sidebar') }}</SheetTitle>
|
||||
<SheetDescription>{{ $t('misc.sidebarDesc') }}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<slot />
|
||||
|
||||
@@ -14,9 +14,9 @@ const { toggleSidebar } = useSidebar()
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
:aria-label="$t('misc.toggleSidebar')"
|
||||
:tabindex="-1"
|
||||
title="Toggle Sidebar"
|
||||
:title="$t('misc.toggleSidebar')"
|
||||
:class="cn(
|
||||
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-0.5 sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
|
||||
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
||||
|
||||
@@ -22,6 +22,6 @@ const { toggleSidebar } = useSidebar()
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<Icon name="ph:sidebar-fill" class="cn-rtl-flip" />
|
||||
<span class="sr-only">Toggle Sidebar</span>
|
||||
<span class="sr-only">{{ $t('misc.toggleSidebar') }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user