181 lines
4.5 KiB
Vue
181 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { useRouter } from 'vue-router'
|
|
import ArticleStatusBadge from '@/components/ArticleStatusBadge.vue'
|
|
import { formatAbsoluteDateFromIso, formatRelativeDate } from '@/lib/relativeDate'
|
|
import type { Article } from '@/composables/useArticles'
|
|
|
|
const props = defineProps<{
|
|
articles: Article[]
|
|
loading: boolean
|
|
busyIds: Set<number>
|
|
now?: Date
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
publish: [id: number]
|
|
unpublish: [id: number]
|
|
delete: [article: Article]
|
|
'copy-url': [id: number]
|
|
}>()
|
|
|
|
const router = useRouter()
|
|
|
|
const columnWidths =
|
|
'[&:nth-child(2)]:w-27.5 [&:nth-child(3)]:w-32.5 [&:nth-child(4)]:w-35 [&:nth-child(5)]:w-11'
|
|
|
|
const columns = [
|
|
{ accessorKey: 'title', header: 'Title' },
|
|
{ accessorKey: 'is_online', header: 'Status', size: 110 },
|
|
{ accessorKey: 'created_at', header: 'Created', size: 130 },
|
|
{ accessorKey: 'updated_at', header: 'Updated', size: 140 },
|
|
{ id: 'actions', header: '', size: 44 },
|
|
]
|
|
|
|
function editPath(article: Article) {
|
|
return `/article/${article.id}/edit`
|
|
}
|
|
|
|
function isBusy(article: Article) {
|
|
return props.busyIds.has(article.id)
|
|
}
|
|
|
|
function pendingFor(article: Article) {
|
|
if (!isBusy(article)) {
|
|
return undefined
|
|
}
|
|
return article.is_online ? ('unpublish' as const) : ('publish' as const)
|
|
}
|
|
|
|
function actionsFor(article: Article) {
|
|
return [
|
|
[
|
|
{
|
|
label: 'Edit',
|
|
icon: 'i-lucide-pencil',
|
|
onSelect: () => router.push(editPath(article)),
|
|
},
|
|
{
|
|
label: article.is_online ? 'Unpublish' : 'Publish',
|
|
icon: article.is_online ? 'i-lucide-globe-lock' : 'i-lucide-globe',
|
|
disabled: isBusy(article),
|
|
onSelect: () => {
|
|
if (article.is_online) {
|
|
emit('unpublish', article.id)
|
|
} else {
|
|
emit('publish', article.id)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
label: 'Copy API URL',
|
|
icon: 'i-lucide-link',
|
|
onSelect: () => emit('copy-url', article.id),
|
|
},
|
|
],
|
|
[
|
|
{
|
|
label: 'Delete…',
|
|
icon: 'i-lucide-trash-2',
|
|
color: 'error' as const,
|
|
onSelect: () => emit('delete', article),
|
|
},
|
|
],
|
|
]
|
|
}
|
|
|
|
function onSelect(row: { original: Article }) {
|
|
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">Loading articles</span>
|
|
<div
|
|
v-for="n in 5"
|
|
: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-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 class="hidden border border-default bg-default md:block">
|
|
<UTable
|
|
:data="articles"
|
|
:columns="columns"
|
|
:ui="{
|
|
base: 'table-fixed',
|
|
tr: 'cursor-pointer',
|
|
th: `px-3 ${columnWidths}`,
|
|
td: `h-11 px-3 py-0 ${columnWidths}`,
|
|
}"
|
|
@select="onSelect"
|
|
>
|
|
<template #caption>
|
|
<caption class="sr-only">
|
|
Articles, sorted by last updated, newest first
|
|
</caption>
|
|
</template>
|
|
|
|
<template #title-cell="{ row }">
|
|
<RouterLink
|
|
:to="editPath(row.original)"
|
|
:data-row-title="row.original.id"
|
|
class="font-medium text-highlighted"
|
|
@click.stop
|
|
>
|
|
{{ row.original.title }}
|
|
</RouterLink>
|
|
</template>
|
|
|
|
<template #is_online-cell="{ row }">
|
|
<ArticleStatusBadge
|
|
:online="row.original.is_online"
|
|
size="sm"
|
|
:pending="pendingFor(row.original)"
|
|
:aria-busy="isBusy(row.original) ? 'true' : undefined"
|
|
/>
|
|
</template>
|
|
|
|
<template #updated_at-header>
|
|
<span class="inline-flex items-center gap-1 text-highlighted">
|
|
Updated
|
|
<UIcon name="i-lucide-arrow-down" class="size-3" />
|
|
</span>
|
|
</template>
|
|
|
|
<template #created_at-cell="{ row }">
|
|
<span class="text-muted">{{
|
|
formatAbsoluteDateFromIso(row.original.created_at)
|
|
}}</span>
|
|
</template>
|
|
|
|
<template #updated_at-cell="{ row }">
|
|
<span 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' }">
|
|
<UButton
|
|
icon="i-lucide-ellipsis"
|
|
color="neutral"
|
|
variant="ghost"
|
|
:aria-label="`Actions for ${row.original.title}`"
|
|
class="size-8 justify-center text-toned"
|
|
@click.stop
|
|
/>
|
|
</UDropdownMenu>
|
|
</template>
|
|
|
|
<template #empty>
|
|
<slot name="empty" />
|
|
</template>
|
|
</UTable>
|
|
</div>
|
|
</template>
|