feat: SEO SPA Sites, Articles and Integration (4/4)

This commit is contained in:
Florian Sylvain committed 2026-10-18 15:51:28 +02:00
1 parent cb737b7ae3
commit 02fb8751fd
47 files changed
+3218 -227

No files matched your search

+54 -29
View File
@@ -4,10 +4,12 @@ import { t } from '@/i18n'
import { computed, ref, watchPostEffect } from 'vue'
import { useRouter } from 'vue-router'
import ArticleStatusBadge from '@/components/ArticleStatusBadge.vue'
import SeoStatusChip from '@/components/SeoStatusChip.vue'
import AuthorStack from '@/components/AuthorStack.vue'
import type { Site } from '@/composables/useSites'
import { formatAbsoluteDateFromIso, formatRelativeDate } from '@/lib/relativeDate'
import type { Article } from '@/composables/useArticles'
import { seoChipOf } from '@/lib/articleSeoStatus'
import { DEFAULT_ARTICLE_SORT, type ArticleSortField } from '@/lib/articleListQuery'
const props = withDefaults(
@@ -20,7 +22,7 @@ const props = withDefaults(
sites?: Site[]
now?: Date
}>(),
{ skeletonRows: 5, sort: DEFAULT_ARTICLE_SORT, sites: () => [] },
{ skeletonRows: 6, sort: DEFAULT_ARTICLE_SORT, sites: () => [] },
)
const emit = defineEmits<{
@@ -38,6 +40,15 @@ const SINGLE_SITE_WIDTHS =
const SEVERAL_SITES_WIDTHS =
'[&:nth-child(2)]:w-26 [&:nth-child(3)]:w-32.5 [&:nth-child(4)]:w-27.5 [&:nth-child(5)]:w-32.5 [&:nth-child(6)]:w-35 [&:nth-child(7)]:w-11'
const rows = computed(() =>
props.loading
? Array.from(
{ length: props.skeletonRows },
(_, index) => ({ id: -(index + 1) }) as Article,
)
: props.articles,
)
const showSite = computed(() => props.sites.length > 1)
const columnWidths = computed(() => (showSite.value ? SEVERAL_SITES_WIDTHS : SINGLE_SITE_WIDTHS))
@@ -212,29 +223,22 @@ function actionsFor(article: Article) {
}
function onSelect(_event: Event, row: { original: Article }) {
if (props.loading) {
return
}
router.push(editPath(row.original))
}
</script>
<template>
<div v-if="loading" aria-busy="true" class="hidden border border-default bg-default md:block">
<span role="status" class="sr-only">{{ t('Loading articles') }}</span>
<div
v-for="n in skeletonRows"
:key="n"
class="flex h-11 items-center gap-3 border-t border-muted px-3 first:border-t-0"
>
<USkeleton class="h-3 max-w-[55%] flex-1 rounded-none bg-accented" />
<USkeleton class="h-6 w-15 rounded-none bg-accented" />
<USkeleton v-if="showSite" class="h-3 w-17.5 rounded-none bg-accented" />
<USkeleton class="h-5 w-15 rounded-none bg-accented" />
<USkeleton class="h-3 w-20 rounded-none bg-accented" />
<USkeleton class="h-3 w-20 rounded-none bg-accented" />
</div>
</div>
<div v-else ref="tableRoot" class="hidden border border-default bg-default md:block">
<div
ref="tableRoot"
:aria-busy="loading ? 'true' : undefined"
class="hidden border border-default bg-default md:block"
>
<span v-if="loading" role="status" class="sr-only">{{ t('Loading articles') }}</span>
<UTable
:data="articles"
:data="rows"
:columns="columns"
:ui="{
base: 'table-fixed',
@@ -257,6 +261,7 @@ function onSelect(_event: Event, row: { original: Article }) {
<template #title-header="{ column }">
<button
type="button"
:disabled="loading"
class="inline-flex items-center gap-1 text-highlighted"
@click.stop="emit('sort', column.id as ArticleSortField)"
>
@@ -273,6 +278,7 @@ function onSelect(_event: Event, row: { original: Article }) {
<template #status-header="{ column }">
<button
type="button"
:disabled="loading"
class="inline-flex items-center gap-1 text-highlighted"
@click.stop="emit('sort', column.id as ArticleSortField)"
>
@@ -289,6 +295,7 @@ function onSelect(_event: Event, row: { original: Article }) {
<template #created_at-header="{ column }">
<button
type="button"
:disabled="loading"
class="inline-flex items-center gap-1 text-highlighted"
@click.stop="emit('sort', column.id as ArticleSortField)"
>
@@ -305,6 +312,7 @@ function onSelect(_event: Event, row: { original: Article }) {
<template #updated_at-header="{ column }">
<button
type="button"
:disabled="loading"
class="inline-flex items-center gap-1 text-highlighted"
@click.stop="emit('sort', column.id as ArticleSortField)"
>
@@ -319,7 +327,9 @@ function onSelect(_event: Event, row: { original: Article }) {
</template>
<template #title-cell="{ row }">
<USkeleton v-if="loading" class="h-3 w-3/5 rounded-none bg-accented" />
<RouterLink
v-else
:to="editPath(row.original)"
:data-row-title="row.original.id"
class="font-medium text-highlighted"
@@ -330,7 +340,8 @@ function onSelect(_event: Event, row: { original: Article }) {
</template>
<template #authors-cell="{ row }">
<span class="flex min-w-0 items-center" :title="authorNames(row.original)">
<USkeleton v-if="loading" class="h-6 w-15 rounded-none bg-accented" />
<span v-else class="flex min-w-0 items-center" :title="authorNames(row.original)">
<AuthorStack
v-if="row.original.authors?.length"
:authors="row.original.authors"
@@ -340,32 +351,46 @@ function onSelect(_event: Event, row: { original: Article }) {
</template>
<template #site-cell="{ row }">
<span class="block truncate text-toned">{{ siteNameOf(row.original) }}</span>
<USkeleton v-if="loading" class="h-3 w-17.5 rounded-none bg-accented" />
<span v-else class="block truncate text-toned">{{ siteNameOf(row.original) }}</span>
</template>
<template #status-cell="{ row }">
<ArticleStatusBadge
:online="row.original.is_online"
size="sm"
:pending="pendingFor(row.original)"
:aria-busy="isBusy(row.original) ? 'true' : undefined"
/>
<USkeleton v-if="loading" class="h-5 w-15 rounded-none bg-accented" />
<span v-else class="flex items-center gap-1.5">
<ArticleStatusBadge
:online="row.original.is_online"
size="md"
:pending="pendingFor(row.original)"
:aria-busy="isBusy(row.original) ? 'true' : undefined"
/>
<SeoStatusChip
v-if="seoChipOf(row.original)"
:chip="seoChipOf(row.original)!"
/>
</span>
</template>
<template #created_at-cell="{ row }">
<span class="text-muted">{{
<USkeleton v-if="loading" class="h-3 w-20 rounded-none bg-accented" />
<span v-else class="text-muted">{{
formatAbsoluteDateFromIso(row.original.created_at)
}}</span>
</template>
<template #updated_at-cell="{ row }">
<span class="text-muted" :title="row.original.updated_at">{{
<USkeleton v-if="loading" class="h-3 w-20 rounded-none bg-accented" />
<span v-else class="text-muted" :title="row.original.updated_at">{{
formatRelativeDate(row.original.updated_at, now)
}}</span>
</template>
<template #actions-cell="{ row }">
<UDropdownMenu :items="actionsFor(row.original)" :content="{ align: 'end' }">
<UDropdownMenu
v-if="!loading"
:items="actionsFor(row.original)"
:content="{ align: 'end' }"
>
<UButton
icon="i-lucide-ellipsis"
color="neutral"