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

218 lines
5.5 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed, ref } from 'vue'
import AuthorAvatar from '@/components/AuthorAvatar.vue'
import AuthorFormPanel from '@/components/AuthorFormPanel.vue'
import ProfileCard from '@/components/ProfileCard.vue'
import { useAuthors, type Author, type MyAuthor } from '@/composables/useAuthors'
import { useMyAuthorProfile } from '@/composables/useMyAuthorProfile'
import { useLocalizedToast } from '@/i18n/presentation'
import { baseSlugOf } from '@/lib/authorProfile'
import { copy } from '@/lib/directoryCopy'
import { useSessionStore } from '@/stores/session'
const SLUG_LOOKUP_SIZE = 25
const session = useSessionStore()
const profile = useMyAuthorProfile()
const { listAuthors } = useAuthors()
const toast = useLocalizedToast()
const failed = ref(false)
const created = ref<MyAuthor | null>(null)
const editing = ref<Author | null>(null)
const loadingEdit = ref(false)
const author = computed(() => session.session?.author)
const canManage = computed(() => session.canManageContent)
const nameLine = computed(() => {
const displayName = session.session?.displayName
return displayName
? t('Uses your display name, {0}. You can change it afterwards.', [displayName])
: t('Uses your username, {0}. You can change it afterwards.', [
session.session?.username ?? '',
])
})
const adjustedSlug = computed(() => {
const current = author.value
const result = created.value
if (!current || !result || result.id !== current.id) {
return null
}
return result.slug_adjusted ? current.slug : null
})
async function create() {
failed.value = false
const outcome = await profile.create()
if (outcome.kind === 'created') {
created.value = outcome.author
}
failed.value = outcome.kind === 'failed'
}
function announceLoadFailed() {
toast.add(() => ({
id: 'author-load-failed',
color: 'error',
icon: 'i-lucide-circle-x',
get title() {
return copy('loadAuthorsFailed')
},
get description() {
return copy('checkConnection')
},
}))
}
async function edit() {
const current = author.value
if (!current || loadingEdit.value) {
return
}
loadingEdit.value = true
try {
const page = await listAuthors({
q: current.slug,
page: 1,
size: SLUG_LOOKUP_SIZE,
})
const full = page.items.find((candidate) => candidate.id === current.id)
if (full) {
editing.value = full
} else {
announceLoadFailed()
}
} catch {
announceLoadFailed()
} finally {
loadingEdit.value = false
}
}
function closePanel() {
editing.value = null
}
function onSaved(saved: Author) {
session.setAuthor({ id: saved.id, name: saved.name, slug: saved.slug })
closePanel()
}
function onUpdated(updated: Author) {
if (updated.user_id === null) {
session.setAuthor(undefined)
closePanel()
return
}
session.setAuthor({
id: updated.id,
name: updated.name,
slug: updated.slug,
})
}
function onGone() {
session.setAuthor(undefined)
closePanel()
}
</script>
<template>
<ProfileCard
:title="t('Author profile')"
:description="t('Your public byline on articles. It\'s separate from your account.')"
>
<template v-if="author">
<div class="flex flex-wrap items-center gap-3">
<AuthorAvatar :name="author.name" :size="40" />
<div class="flex min-w-35 flex-1 flex-col">
<span class="font-medium text-highlighted">{{ author.name }}</span>
<span class="font-mono text-xs/4.5 text-muted">{{ author.slug }}</span>
</div>
<UButton
color="neutral"
variant="outline"
icon="i-lucide-pencil"
:loading="loadingEdit"
class="h-11 justify-center border border-accented bg-default px-3 text-highlighted ring-0 lg:h-8"
@click="edit"
>
{{ t('Edit author') }}
</UButton>
</div>
<UAlert
v-if="adjustedSlug"
role="status"
color="info"
variant="soft"
icon="i-lucide-info"
:title="t('Your slug is {0}', [adjustedSlug])"
:description="
t('{0} was already used by another author. You can change it in Edit author.', [
baseSlugOf(adjustedSlug),
])
"
:close="false"
:ui="{
root: 'items-start gap-2.5 px-3.5 py-3',
title: 'font-semibold',
description: 'text-toned',
}"
/>
<p class="flex items-center gap-1.5 text-xs text-muted">
<UIcon name="i-lucide-link" class="size-3 shrink-0" />
{{ t('Linked to your account. New articles start with this author.') }}
</p>
</template>
<template v-else>
<UAlert
v-if="failed"
role="alert"
color="error"
variant="soft"
icon="i-lucide-circle-x"
:title="t('Couldn\'t create your author profile')"
:description="t('Try again in a moment.')"
:close="false"
:ui="{
root: 'items-start gap-2.5 px-3.5 py-3',
title: 'font-semibold',
description: 'text-toned',
}"
/>
<p class="text-pretty text-toned">
{{
t(
"You don't have an author profile yet. Create one to be credited on your own articles.",
)
}}
</p>
<div class="flex flex-col gap-3 lg:flex-row lg:items-end">
<UButton
icon="i-lucide-feather"
:loading="profile.pending.value"
:disabled="profile.pending.value"
class="h-11 justify-center px-3 lg:h-8"
@click="create"
>
{{ profile.pending.value ? t('Creating…') : t('Create my author profile') }}
</UButton>
<span class="text-xs text-muted">{{ nameLine }}</span>
</div>
</template>
<AuthorFormPanel
v-if="editing"
:author="editing"
:own-profile="!canManage"
:own-email="session.session?.email ?? ''"
@close="closePanel"
@saved="onSaved"
@updated="onUpdated"
@gone="onGone"
/>
</ProfileCard>
</template>