feat(SPA): media page T0
This commit is contained in:
1 parent
0d99544579
commit
67d456baf6
32 files changed
+4874
-40
No files matched your search
@@ -0,0 +1,214 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import MediaArticleStatusBadge from '@/components/MediaArticleStatusBadge.vue'
|
||||
import MediaThumb from '@/components/MediaThumb.vue'
|
||||
import type { MediaItem, MediaUsageItem } from '@/composables/useMedia'
|
||||
import {
|
||||
formatDimensions,
|
||||
formatMediaDate,
|
||||
formatSize,
|
||||
formatType,
|
||||
pluralArticles,
|
||||
} from '@/lib/mediaFormat'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const ACTION_BUTTON =
|
||||
'h-11 justify-center border border-accented bg-default px-3 text-sm ring-0 max-md:flex-1 md:h-8'
|
||||
|
||||
const props = defineProps<{
|
||||
item: MediaItem | null
|
||||
usage: MediaUsageItem[]
|
||||
copies: MediaItem[]
|
||||
offline: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'copy-url': []
|
||||
rename: []
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
|
||||
const closeButton = computed(() => ({
|
||||
color: 'neutral' as const,
|
||||
variant: 'ghost' as const,
|
||||
icon: isDesktop.value ? 'i-lucide-x' : 'i-lucide-arrow-left',
|
||||
class: 'text-muted',
|
||||
}))
|
||||
|
||||
const metadata = computed(() => {
|
||||
const item = props.item
|
||||
if (!item) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{ label: 'Type', value: formatType(item.content_type) },
|
||||
{ label: 'Size', value: formatSize(item.size) },
|
||||
{ label: 'Dimensions', value: formatDimensions(item.width, item.height) },
|
||||
{ label: 'Uploaded', value: formatMediaDate(item.created_at) },
|
||||
{ label: 'Uploaded by', value: item.uploaded_by },
|
||||
]
|
||||
})
|
||||
|
||||
const displayUrl = computed(() => `${window.location.host}${props.item?.url ?? ''}`)
|
||||
|
||||
function joinNames(names: string[]) {
|
||||
if (names.length <= 1) {
|
||||
return names.join('')
|
||||
}
|
||||
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`
|
||||
}
|
||||
|
||||
const duplicateAlert = computed(() => {
|
||||
if (!props.item?.duplicate || props.copies.length === 0) {
|
||||
return null
|
||||
}
|
||||
const names = props.copies.map((copy) => copy.filename)
|
||||
return {
|
||||
title: `Identical to ${props.copies.length} other ${props.copies.length === 1 ? 'image' : 'images'}`,
|
||||
description: `${joinNames(names)} ${names.length === 1 ? 'has' : 'have'} the same content.`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<USlideover
|
||||
v-model:open="open"
|
||||
side="right"
|
||||
:title="item?.filename ?? ''"
|
||||
description="Image details"
|
||||
:close="closeButton"
|
||||
:ui="{
|
||||
content: 'w-full max-w-none md:max-w-115',
|
||||
header: 'h-14 min-h-14 border-b border-default px-5 py-0 sm:px-5 max-md:px-1.5 max-md:sm:px-1.5',
|
||||
wrapper: 'min-w-0 flex-1 max-md:ps-11 md:pe-9',
|
||||
title: 'truncate text-lg font-semibold text-highlighted',
|
||||
description: 'sr-only',
|
||||
close: 'top-1.75 size-11 justify-center p-0 max-md:start-1.5 md:top-3.5 md:end-5 md:size-7',
|
||||
body: 'p-4 md:p-5',
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="item" class="flex flex-col gap-5">
|
||||
<MediaThumb
|
||||
:item="item"
|
||||
size="xl"
|
||||
class="aspect-4/3 w-full border border-default md:aspect-16/10"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2 max-md:flex-wrap">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-link"
|
||||
:class="ACTION_BUTTON"
|
||||
@click="emit('copy-url')"
|
||||
>
|
||||
Copy URL
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
:disabled="offline"
|
||||
:class="ACTION_BUTTON"
|
||||
@click="emit('rename')"
|
||||
>
|
||||
Rename
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash-2"
|
||||
:disabled="offline"
|
||||
:class="ACTION_BUTTON"
|
||||
class="text-error"
|
||||
@click="emit('delete')"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="duplicateAlert"
|
||||
data-testid="duplicate-alert"
|
||||
color="info"
|
||||
variant="soft"
|
||||
orientation="vertical"
|
||||
:close="false"
|
||||
icon="i-lucide-copy"
|
||||
:title="duplicateAlert.title"
|
||||
:description="duplicateAlert.description"
|
||||
:ui="{ actions: 'mt-2 w-full', description: 'text-wrap' }"
|
||||
>
|
||||
<template #actions>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-arrow-right"
|
||||
disabled
|
||||
block
|
||||
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7"
|
||||
:ui="{ leadingIcon: 'size-3.5' }"
|
||||
>
|
||||
Review duplicates
|
||||
</UButton>
|
||||
</template>
|
||||
</UAlert>
|
||||
|
||||
<dl class="grid grid-cols-[110px_minmax(0,1fr)] gap-x-3 gap-y-2 text-toned">
|
||||
<template v-for="row in metadata" :key="row.label">
|
||||
<dt class="text-muted">{{ row.label }}</dt>
|
||||
<dd class="m-0 text-highlighted">{{ row.value }}</dd>
|
||||
</template>
|
||||
<dt class="text-muted">URL</dt>
|
||||
<dd class="m-0 truncate font-mono text-xs/5 text-highlighted">
|
||||
{{ displayUrl }}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<section class="flex flex-col gap-2">
|
||||
<h3 class="text-base font-semibold text-highlighted">
|
||||
{{
|
||||
item.used_in > 0
|
||||
? `Used in ${pluralArticles(item.used_in)}`
|
||||
: 'Not used in any article'
|
||||
}}
|
||||
</h3>
|
||||
<template v-if="item.used_in > 0">
|
||||
<ul v-if="usage.length > 0" class="border border-default">
|
||||
<li
|
||||
v-for="(article, index) in usage"
|
||||
:key="article.id"
|
||||
data-testid="usage-row"
|
||||
class="flex min-h-12 items-center gap-2 px-3 md:min-h-10"
|
||||
:class="{ 'border-t border-muted': index > 0 }"
|
||||
>
|
||||
<RouterLink
|
||||
:to="`/article/${article.id}/edit`"
|
||||
class="min-w-0 flex-1 truncate font-medium text-highlighted underline underline-offset-3"
|
||||
>
|
||||
{{ article.title }}
|
||||
</RouterLink>
|
||||
<MediaArticleStatusBadge :published="article.published" size="md" />
|
||||
<UIcon
|
||||
name="i-lucide-arrow-up-right"
|
||||
class="size-3.5 shrink-0 text-muted"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-xs text-muted">
|
||||
Counted from the text of your articles each time you open this page.
|
||||
</p>
|
||||
</template>
|
||||
<p v-else class="text-muted">
|
||||
This image is safe to delete. No article shows it.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
Reference in new issue
Block a user