fix: multiple fixes all accross the SPA, missing buttons actions, styling etc.
This commit is contained in:
1 parent
ad55137883
commit
d4fcfb2bba
11 files changed
+262
-43
No files matched your search
@@ -8,6 +8,7 @@
|
|||||||
"@nuxt/ui": "^4.11.2",
|
"@nuxt/ui": "^4.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tiptap/core": "3.31.3",
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/extension-drag-handle-vue-3": "^3.31.3",
|
||||||
"@vueuse/core": "14.4.0",
|
"@vueuse/core": "14.4.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"shiki": "^4.4.3",
|
"shiki": "^4.4.3",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@nuxt/ui": "^4.11.2",
|
"@nuxt/ui": "^4.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tiptap/core": "3.31.3",
|
"@tiptap/core": "3.31.3",
|
||||||
|
"@tiptap/extension-drag-handle-vue-3": "^3.31.3",
|
||||||
"@vueuse/core": "14.4.0",
|
"@vueuse/core": "14.4.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"shiki": "^4.4.3",
|
"shiki": "^4.4.3",
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useToast } from '@nuxt/ui/composables'
|
||||||
import type { Article } from '@/composables/useArticles'
|
import type { Article } from '@/composables/useArticles'
|
||||||
|
|
||||||
const props = defineProps<{ article: Article }>()
|
const props = defineProps<{ article: Article }>()
|
||||||
|
|
||||||
defineEmits<{ 'copy-url': [] }>()
|
const emit = defineEmits<{ 'copy-url': [] }>()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false })
|
const open = defineModel<boolean>('open', { default: false })
|
||||||
|
|
||||||
@@ -55,6 +58,28 @@ const apiNote = computed(() =>
|
|||||||
? 'Live. Your website can read this article.'
|
? 'Live. Your website can read this article.'
|
||||||
: 'Returns 404 until the article is online.',
|
: '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',
|
||||||
|
title: 'Link copied',
|
||||||
|
description: `The API URL for "${props.article.title}" is on your clipboard.`,
|
||||||
|
})
|
||||||
|
emit('copy-url')
|
||||||
|
} catch {
|
||||||
|
toast.add({
|
||||||
|
id: `article-link-copy-failed-${props.article.id}`,
|
||||||
|
color: 'error',
|
||||||
|
icon: 'i-lucide-circle-x',
|
||||||
|
title: "Couldn't copy the link",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -89,7 +114,15 @@ const apiNote = computed(() =>
|
|||||||
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
|
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
|
||||||
>
|
>
|
||||||
<span class="flex-1">{{ publicUrl }}</span>
|
<span class="flex-1">{{ publicUrl }}</span>
|
||||||
<UIcon name="i-lucide-copy" class="size-3.5" />
|
<UButton
|
||||||
|
icon="i-lucide-copy"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Copy the public API URL"
|
||||||
|
class="size-5 p-0"
|
||||||
|
@click="copyUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
|
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +160,15 @@ const apiNote = computed(() =>
|
|||||||
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
|
class="flex items-center gap-1.5 bg-elevated px-2 py-1.5 font-mono text-xs"
|
||||||
>
|
>
|
||||||
<span class="flex-1">{{ publicUrl }}</span>
|
<span class="flex-1">{{ publicUrl }}</span>
|
||||||
<UIcon name="i-lucide-copy" class="size-3.5" />
|
<UButton
|
||||||
|
icon="i-lucide-copy"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Copy the public API URL"
|
||||||
|
class="size-5 p-0"
|
||||||
|
@click="copyUrl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
|
<p class="text-xs/[18px] text-muted">{{ apiNote }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ defineExpose({
|
|||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
>
|
>
|
||||||
<span class="hidden">{{ captureEditor(editor) }}</span>
|
<span class="hidden">{{ captureEditor(editor) }}</span>
|
||||||
|
<UEditorDragHandle :editor="editor" />
|
||||||
<UEditorToolbar
|
<UEditorToolbar
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
:items="buildItems(editor)"
|
:items="buildItems(editor)"
|
||||||
@@ -239,16 +240,16 @@ defineExpose({
|
|||||||
grid-row-start: 2;
|
grid-row-start: 2;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 2.5rem 3rem;
|
|
||||||
background: var(--ui-bg);
|
background: var(--ui-bg);
|
||||||
}
|
}
|
||||||
.article-editor :deep(.ProseMirror) {
|
.article-editor :deep(.ProseMirror) {
|
||||||
max-width: 42rem;
|
box-sizing: border-box;
|
||||||
padding: 0;
|
max-width: 48rem;
|
||||||
|
padding: 2.5rem 3rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 47.999rem) {
|
@media (max-width: 47.999rem) {
|
||||||
.article-editor :deep([data-slot='content']) {
|
.article-editor :deep(.ProseMirror) {
|
||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ArticleStatusBadge from '@/components/ArticleStatusBadge.vue'
|
import ArticleStatusBadge from '@/components/ArticleStatusBadge.vue'
|
||||||
import { formatAbsoluteDateFromIso, formatRelativeDate } from '@/lib/relativeDate'
|
import { formatAbsoluteDateFromIso, formatRelativeDate } from '@/lib/relativeDate'
|
||||||
@@ -23,14 +24,27 @@ const router = useRouter()
|
|||||||
const columnWidths =
|
const columnWidths =
|
||||||
'[&:nth-child(2)]:w-27.5 [&:nth-child(3)]:w-32.5 [&:nth-child(4)]:w-35 [&:nth-child(5)]:w-11'
|
'[&:nth-child(2)]:w-27.5 [&:nth-child(3)]:w-32.5 [&:nth-child(4)]:w-35 [&:nth-child(5)]:w-11'
|
||||||
|
|
||||||
|
const sorting = ref([{ id: 'updated_at', desc: true }])
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ accessorKey: 'title', header: 'Title' },
|
{ accessorKey: 'title', header: 'Title', enableSorting: true },
|
||||||
{ accessorKey: 'is_online', header: 'Status', size: 110 },
|
{ accessorKey: 'is_online', header: 'Status', size: 110, enableSorting: true },
|
||||||
{ accessorKey: 'created_at', header: 'Created', size: 130 },
|
{ accessorKey: 'created_at', header: 'Created', size: 130, enableSorting: true },
|
||||||
{ accessorKey: 'updated_at', header: 'Updated', size: 140 },
|
{ accessorKey: 'updated_at', header: 'Updated', size: 140, enableSorting: true },
|
||||||
{ id: 'actions', header: '', size: 44 },
|
{ id: 'actions', header: '', size: 44 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function sortIcon(column: { getIsSorted: () => false | 'asc' | 'desc' }) {
|
||||||
|
const sorted = column.getIsSorted()
|
||||||
|
if (sorted === 'asc') {
|
||||||
|
return 'i-lucide-arrow-up'
|
||||||
|
}
|
||||||
|
if (sorted === 'desc') {
|
||||||
|
return 'i-lucide-arrow-down'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function editPath(article: Article) {
|
function editPath(article: Article) {
|
||||||
return `/article/${article.id}/edit`
|
return `/article/${article.id}/edit`
|
||||||
}
|
}
|
||||||
@@ -112,6 +126,7 @@ function onSelect(row: { original: Article }) {
|
|||||||
th: `px-3 ${columnWidths}`,
|
th: `px-3 ${columnWidths}`,
|
||||||
td: `h-11 px-3 py-0 ${columnWidths}`,
|
td: `h-11 px-3 py-0 ${columnWidths}`,
|
||||||
}"
|
}"
|
||||||
|
v-model:sorting="sorting"
|
||||||
@select="onSelect"
|
@select="onSelect"
|
||||||
>
|
>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
@@ -120,6 +135,50 @@ function onSelect(row: { original: Article }) {
|
|||||||
</caption>
|
</caption>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #title-header="{ column }">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-highlighted"
|
||||||
|
@click.stop="column.toggleSorting()"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
<UIcon v-if="sortIcon(column)" :name="sortIcon(column)!" class="size-3" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #is_online-header="{ column }">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-highlighted"
|
||||||
|
@click.stop="column.toggleSorting()"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
<UIcon v-if="sortIcon(column)" :name="sortIcon(column)!" class="size-3" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #created_at-header="{ column }">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-highlighted"
|
||||||
|
@click.stop="column.toggleSorting()"
|
||||||
|
>
|
||||||
|
Created
|
||||||
|
<UIcon v-if="sortIcon(column)" :name="sortIcon(column)!" class="size-3" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #updated_at-header="{ column }">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center gap-1 text-highlighted"
|
||||||
|
@click.stop="column.toggleSorting()"
|
||||||
|
>
|
||||||
|
Updated
|
||||||
|
<UIcon v-if="sortIcon(column)" :name="sortIcon(column)!" class="size-3" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #title-cell="{ row }">
|
<template #title-cell="{ row }">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="editPath(row.original)"
|
:to="editPath(row.original)"
|
||||||
@@ -140,13 +199,6 @@ function onSelect(row: { original: Article }) {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</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 }">
|
<template #created_at-cell="{ row }">
|
||||||
<span class="text-muted">{{
|
<span class="text-muted">{{
|
||||||
formatAbsoluteDateFromIso(row.original.created_at)
|
formatAbsoluteDateFromIso(row.original.created_at)
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ const props = withDefaults(
|
|||||||
min?: number
|
min?: number
|
||||||
warnAt?: number
|
warnAt?: number
|
||||||
variant?: 'inline' | 'footer'
|
variant?: 'inline' | 'footer'
|
||||||
|
plainText?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
min: 0,
|
min: 0,
|
||||||
warnAt: 0.9,
|
warnAt: 0.9,
|
||||||
variant: 'inline',
|
variant: 'inline',
|
||||||
|
plainText: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,11 +36,14 @@ const status = computed<'normal' | 'warn' | 'over' | 'under'>(() => {
|
|||||||
return 'normal'
|
return 'normal'
|
||||||
})
|
})
|
||||||
|
|
||||||
const overText = computed(() =>
|
const overText = computed(() => {
|
||||||
overBy.value === 1
|
const suffix = props.plainText
|
||||||
? '1 character over. Shorten the text or remove an image. Formatting counts too.'
|
? 'Shorten the text.'
|
||||||
: `${formatThousands(overBy.value)} characters over. Shorten the text or remove an image. Formatting counts too.`,
|
: 'Shorten the text or remove an image. Formatting counts too.'
|
||||||
)
|
return overBy.value === 1
|
||||||
|
? `1 character over. ${suffix}`
|
||||||
|
: `${formatThousands(overBy.value)} characters over. ${suffix}`
|
||||||
|
})
|
||||||
|
|
||||||
const warnText = 'Close to the limit. Images and formatting count too.'
|
const warnText = 'Close to the limit. Images and formatting count too.'
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from '@/composables/useApi'
|
||||||
|
import { useSidebarCollapsed } from '@/composables/useSidebarCollapsed'
|
||||||
import AppBoot from '@/components/AppBoot.vue'
|
import AppBoot from '@/components/AppBoot.vue'
|
||||||
import AppLogo from '@/components/AppLogo.vue'
|
import AppLogo from '@/components/AppLogo.vue'
|
||||||
import ErrorView from '@/components/ErrorView.vue'
|
import ErrorView from '@/components/ErrorView.vue'
|
||||||
@@ -44,7 +45,22 @@ const navItems = computed(() =>
|
|||||||
|
|
||||||
const routeTitle = computed(() => (route.meta.title as string) ?? '')
|
const routeTitle = computed(() => (route.meta.title as string) ?? '')
|
||||||
const pageTitle = computed(() => props.mobileTitle ?? routeTitle.value)
|
const pageTitle = computed(() => props.mobileTitle ?? routeTitle.value)
|
||||||
const sidebarCollapsed = ref(false)
|
const sidebarCollapsed = useSidebarCollapsed()
|
||||||
|
|
||||||
|
const sidebarUi = computed(() => ({
|
||||||
|
root: sidebarCollapsed.value ? 'min-w-14 px-1 py-3' : 'min-w-14 p-3',
|
||||||
|
header: 'mb-3 h-10 gap-2.5 px-1.5',
|
||||||
|
body: 'gap-0.5 p-0',
|
||||||
|
footer: 'p-0',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const navUi = computed(() => ({
|
||||||
|
list: 'flex flex-col gap-0.5',
|
||||||
|
link: sidebarCollapsed.value
|
||||||
|
? 'h-8 justify-center gap-0 px-0 text-sm'
|
||||||
|
: 'h-8 gap-2.5 px-2.5 py-0 text-sm',
|
||||||
|
linkLeadingIcon: 'size-4',
|
||||||
|
}))
|
||||||
const loggingOut = ref(false)
|
const loggingOut = ref(false)
|
||||||
|
|
||||||
function onRetry() {
|
function onRetry() {
|
||||||
@@ -75,12 +91,7 @@ async function onLogout() {
|
|||||||
:resizable="false"
|
:resizable="false"
|
||||||
:default-size="14"
|
:default-size="14"
|
||||||
:collapsed-size="3.5"
|
:collapsed-size="3.5"
|
||||||
:ui="{
|
:ui="sidebarUi"
|
||||||
root: 'min-w-14 p-3',
|
|
||||||
header: 'mb-3 h-10 gap-2.5 px-1.5',
|
|
||||||
body: 'gap-0.5 p-0',
|
|
||||||
footer: 'p-0',
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<AppLogo :size="26" />
|
<AppLogo :size="26" />
|
||||||
@@ -97,11 +108,7 @@ async function onLogout() {
|
|||||||
:items="navItems"
|
:items="navItems"
|
||||||
:collapsed="sidebarCollapsed"
|
:collapsed="sidebarCollapsed"
|
||||||
aria-label="Main"
|
aria-label="Main"
|
||||||
:ui="{
|
:ui="navUi"
|
||||||
list: 'flex flex-col gap-0.5',
|
|
||||||
link: 'h-8 gap-2.5 px-2.5 py-0 text-sm',
|
|
||||||
linkLeadingIcon: 'size-4',
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ function onCancel() {
|
|||||||
:value="titleLength"
|
:value="titleLength"
|
||||||
:max="TITLE_MAX_LENGTH"
|
:max="TITLE_MAX_LENGTH"
|
||||||
:min="titleCounterMin"
|
:min="titleCounterMin"
|
||||||
|
plain-text
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="titleError" #error="{ error }">
|
<template v-if="titleError" #error="{ error }">
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ const items = computed(() => [
|
|||||||
color="neutral"
|
color="neutral"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
block
|
block
|
||||||
class="h-10 justify-start gap-2.5 px-1.5 font-normal text-highlighted"
|
:class="[
|
||||||
trailing-icon="i-lucide-chevrons-up-down"
|
'h-10 gap-2.5 px-1.5 font-normal text-highlighted',
|
||||||
|
collapsed ? 'justify-center' : 'justify-start',
|
||||||
|
]"
|
||||||
|
:trailing-icon="collapsed ? undefined : 'i-lucide-chevrons-up-down'"
|
||||||
:ui="{ trailingIcon: 'ms-auto size-3.5 text-default' }"
|
:ui="{ trailingIcon: 'ms-auto size-3.5 text-default' }"
|
||||||
:aria-label="triggerAriaLabel"
|
:aria-label="triggerAriaLabel"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
|
export function useSidebarCollapsed() {
|
||||||
|
return useStorage('sidebar-collapsed', false)
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ const showSkeleton = ref(false)
|
|||||||
const article = ref<Article | null>(null)
|
const article = ref<Article | null>(null)
|
||||||
const previewOn = ref(false)
|
const previewOn = ref(false)
|
||||||
const detailsOpen = ref(false)
|
const detailsOpen = ref(false)
|
||||||
|
const publishPending = ref<string | undefined>(undefined)
|
||||||
let skeletonTimer: ReturnType<typeof setTimeout> | null = null
|
let skeletonTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
function clearSkeletonTimer() {
|
function clearSkeletonTimer() {
|
||||||
@@ -111,6 +112,99 @@ function goBack() {
|
|||||||
router.push('/article')
|
router.push('/article')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setOnline(makeOnline: boolean) {
|
||||||
|
if (!article.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
publishPending.value = makeOnline ? 'Publishing…' : 'Unpublishing…'
|
||||||
|
try {
|
||||||
|
const updated = await request<Article>(
|
||||||
|
`/v1/article/${articleId}/${makeOnline ? 'publish' : 'unpublish'}`,
|
||||||
|
{ method: 'PATCH' },
|
||||||
|
)
|
||||||
|
article.value = updated
|
||||||
|
toast.add({
|
||||||
|
id: `article-${makeOnline ? 'published' : 'unpublished'}-${articleId}`,
|
||||||
|
color: 'success',
|
||||||
|
icon: 'i-lucide-circle-check',
|
||||||
|
title: makeOnline ? 'Article published' : 'Article unpublished',
|
||||||
|
description: makeOnline
|
||||||
|
? `"${updated.title}" is now online.`
|
||||||
|
: `"${updated.title}" is now offline.`,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && err.status === 404) {
|
||||||
|
loadState.value = 'notFound'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.add({
|
||||||
|
id: `article-${makeOnline ? 'publish' : 'unpublish'}-failed-${articleId}`,
|
||||||
|
color: 'error',
|
||||||
|
icon: 'i-lucide-circle-x',
|
||||||
|
title: makeOnline ? "Couldn't publish" : "Couldn't unpublish",
|
||||||
|
description: "The server didn't respond. Nothing changed.",
|
||||||
|
duration: 0,
|
||||||
|
actions: [{ label: 'Retry', onClick: () => setOnline(makeOnline) }],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
publishPending.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePublish() {
|
||||||
|
setOnline(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUnpublish() {
|
||||||
|
setOnline(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDescription() {
|
||||||
|
const count = article.value?.images.length ?? 0
|
||||||
|
const imagesPhrase =
|
||||||
|
count === 0 ? '' : count === 1 ? ' and its image' : ` and its ${count} images`
|
||||||
|
return `"${article.value?.title ?? ''}"${imagesPhrase} will be deleted. This can't be undone.`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performDelete() {
|
||||||
|
try {
|
||||||
|
await request(`/v1/article/${articleId}`, { method: 'DELETE' })
|
||||||
|
toast.add({
|
||||||
|
id: `article-deleted-${articleId}`,
|
||||||
|
color: 'success',
|
||||||
|
icon: 'i-lucide-circle-check',
|
||||||
|
title: 'Article deleted',
|
||||||
|
description: `"${article.value?.title ?? ''}" and its images are gone.`,
|
||||||
|
})
|
||||||
|
router.push('/article')
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError && err.status === 404) {
|
||||||
|
loadState.value = 'notFound'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw new Error("Couldn't delete the article, so it's still there. Try again in a moment.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!article.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await overlay
|
||||||
|
.create(ConfirmDialog, {
|
||||||
|
props: {
|
||||||
|
title: 'Delete this article?',
|
||||||
|
description: deleteDescription(),
|
||||||
|
confirmLabel: 'Delete article',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
pendingLabel: 'Deleting…',
|
||||||
|
tone: 'danger',
|
||||||
|
action: performDelete,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.open()
|
||||||
|
}
|
||||||
|
|
||||||
function onKeydown(event: KeyboardEvent) {
|
function onKeydown(event: KeyboardEvent) {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -254,7 +348,7 @@ onBeforeRouteLeave(async () => {
|
|||||||
root: 'items-center gap-2',
|
root: 'items-center gap-2',
|
||||||
container: 'h-auto',
|
container: 'h-auto',
|
||||||
base: 'h-4.5 w-8 p-0.5',
|
base: 'h-4.5 w-8 p-0.5',
|
||||||
thumb: 'size-3.5 bg-default data-[state=checked]:translate-x-3.5',
|
thumb: 'size-3.5 bg-default data-[state=checked]:translate-x-2.5',
|
||||||
wrapper: 'ms-0',
|
wrapper: 'ms-0',
|
||||||
label: 'text-sm font-normal text-toned',
|
label: 'text-sm font-normal text-toned',
|
||||||
}"
|
}"
|
||||||
@@ -274,15 +368,20 @@ onBeforeRouteLeave(async () => {
|
|||||||
class="h-8 gap-1.5 px-3"
|
class="h-8 gap-1.5 px-3"
|
||||||
:online="article.is_online"
|
:online="article.is_online"
|
||||||
:dirty="draft.state.value === 'dirty'"
|
:dirty="draft.state.value === 'dirty'"
|
||||||
|
:pending="publishPending"
|
||||||
:disabled="publishDisabled"
|
:disabled="publishDisabled"
|
||||||
|
@publish="handlePublish"
|
||||||
|
@unpublish="handleUnpublish"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-lucide-ellipsis"
|
icon="i-lucide-trash-2"
|
||||||
color="neutral"
|
color="error"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="size-8 justify-center text-toned"
|
class="h-8 gap-1.5 px-3"
|
||||||
aria-label="More actions"
|
@click="handleDelete"
|
||||||
/>
|
>
|
||||||
|
Delete
|
||||||
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,7 +456,10 @@ onBeforeRouteLeave(async () => {
|
|||||||
hide-icon
|
hide-icon
|
||||||
:online="article.is_online"
|
:online="article.is_online"
|
||||||
:dirty="draft.state.value === 'dirty'"
|
:dirty="draft.state.value === 'dirty'"
|
||||||
|
:pending="publishPending"
|
||||||
:disabled="publishDisabled"
|
:disabled="publishDisabled"
|
||||||
|
@publish="handlePublish"
|
||||||
|
@unpublish="handleUnpublish"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user