feat: sites and authors integration into existing screens (& formated all frontend files kinda accidentally)
This commit is contained in:
1 parent
3d81d2c717
commit
6b8990c55e
239 files changed
+9194
-1354
No files matched your search
@@ -0,0 +1,217 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user