Files
RenewCMS/web/src/components/ArticleDetails.vue
T

207 lines
6.0 KiB
Vue

<script setup lang="ts">
import { t, locale, formatNumber } from '@/i18n'
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
import { computed } from 'vue'
import { useLocalizedToast } from '@/i18n/presentation'
import type { Article } from '@/composables/useArticles'
import { bodyImageCount } from '@/lib/bodyImageIds'
const props = defineProps<{ article: Article; multilingual?: boolean }>()
const emit = defineEmits<{ 'copy-url': [] }>()
const toast = useLocalizedToast()
const open = defineModel<boolean>('open', { default: false })
const titleNote = computed(() =>
props.multilingual
? t(
"Each language has its own title. Changing it doesn't change the URL; edit the slug in Search and sharing.",
)
: t("Changing the title doesn't change the URL; edit the slug in Search and sharing."),
)
const MONTH_NAMES = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
function isSameDay(a: Date, b: Date) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
)
}
function formatDetailDate(iso: string, relative = false) {
const date = new Date(iso)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
if (relative && isSameDay(date, new Date())) {
return t('Today, {0}:{1}', [hours, minutes])
}
return locale.value === 'fr'
? `${formatAbsoluteDateFromIso(iso)}, ${hours}:${minutes}`
: `${date.getDate()} ${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}, ${hours}:${minutes}`
}
const createdLabel = computed(() => formatDetailDate(props.article.created_at))
const updatedLabel = computed(() => formatDetailDate(props.article.updated_at, true))
const publicUrl = computed(() => `/v1/article/${props.article.id}`)
const imagesCount = computed(() => bodyImageCount(props.article.body))
const apiNote = computed(() =>
props.article.is_online
? t('Live. Your website can read this article.')
: t('Returns 404 until the article is online.'),
)
async function copyUrl() {
const url = `${window.location.origin}${publicUrl.value}`
try {
await navigator.clipboard.writeText(url)
toast.add(() => ({
id: `article-link-copied-${props.article.id}`,
color: 'success',
icon: 'i-lucide-circle-check',
get title() {
return t('Link copied')
},
get description() {
return t('The API URL for \u0022{0}\u0022 is on your clipboard.', [
props.article.title,
])
},
}))
emit('copy-url')
} catch {
toast.add(() => ({
id: `article-link-copy-failed-${props.article.id}`,
color: 'error',
icon: 'i-lucide-circle-x',
get title() {
return t("Couldn't copy the link")
},
}))
}
}
</script>
<template>
<div>
<UCard
data-testid="article-details"
class="hidden lg:flex"
:ui="{
root: 'flex-col gap-4 rounded-none bg-transparent shadow-none ring-0 divide-y-0',
header: 'p-0 sm:p-0',
body: 'flex flex-col gap-4 p-0 sm:p-0',
}"
>
<template #header>
<h2 class="text-base font-semibold text-highlighted">
{{ t('Details') }}
</h2>
</template>
<slot name="meta" :sheet="false" />
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 text-sm">
<dt class="text-muted">{{ t('Created') }}</dt>
<dd class="text-highlighted">{{ createdLabel }}</dd>
<dt class="text-muted">{{ t('Updated') }}</dt>
<dd class="text-highlighted">{{ updatedLabel }}</dd>
<dt class="text-muted">ID</dt>
<dd class="font-mono text-highlighted">{{ article.id }}</dd>
<dt class="text-muted">{{ t('Images') }}</dt>
<dd class="text-highlighted">
{{ formatNumber(imagesCount) }}
</dd>
</dl>
<div class="flex flex-col gap-1.5 text-sm">
<p class="text-muted">{{ t('Public API URL') }}</p>
<div class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs">
<span class="flex-1">{{ publicUrl }}</span>
<UButton
icon="i-lucide-copy"
color="neutral"
variant="ghost"
size="sm"
:aria-label="t('Copy the public API URL')"
class="size-5 p-0"
@click="copyUrl"
/>
</div>
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
</div>
<div class="flex flex-col gap-1 border-t border-default pt-3 text-sm">
<p class="text-muted">{{ t('Title') }}</p>
<p class="text-xs/[18px] text-toned">{{ titleNote }}</p>
</div>
</UCard>
<USlideover
v-model:open="open"
side="bottom"
:title="t('Details')"
:close="{
'aria-label': t('Close'),
class: 'size-11 justify-center text-muted',
ui: { leadingIcon: 'size-4' },
}"
class="lg:hidden"
:ui="{
content:
'h-23/25 max-h-none pt-3 before:absolute before:top-2 before:left-1/2 before:h-1 before:w-9 before:-translate-x-1/2 before:bg-(--ui-border-accented)',
header: 'min-h-13 items-center gap-3 border-b-0 px-4 pt-2 pb-0 sm:px-4',
title: 'text-lg/6 font-semibold',
description: 'sr-only',
body: 'flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-4 pt-3 pb-4 sm:px-4',
}"
>
<template #body>
<div class="flex flex-col gap-4 text-sm">
<slot name="meta" :sheet="true" />
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-2">
<dt class="text-muted">{{ t('Created') }}</dt>
<dd class="text-highlighted">{{ createdLabel }}</dd>
<dt class="text-muted">{{ t('Updated') }}</dt>
<dd class="text-highlighted">{{ updatedLabel }}</dd>
<dt class="text-muted">ID</dt>
<dd class="font-mono text-highlighted">
{{ article.id }}
</dd>
</dl>
<div class="flex flex-col gap-1.5">
<p class="text-muted">{{ t('Public API URL') }}</p>
<div
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
>
<span class="flex-1">{{ publicUrl }}</span>
<UButton
icon="i-lucide-copy"
color="neutral"
variant="ghost"
size="sm"
:aria-label="t('Copy the public API URL')"
class="size-5 p-0"
@click="copyUrl"
/>
</div>
</div>
</div>
</template>
</USlideover>
</div>
</template>