207 lines
6.0 KiB
Vue
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>
|