feat: SEO SPA editor integration
This commit is contained in:
1 parent
eb9876fde3
commit
470584400f
62 files changed
+7471
-58
No files matched your search
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, useId } from 'vue'
|
||||
import SuggestionList from '@/components/SuggestionList.vue'
|
||||
import { isChecking, type SeoCheckPhase } from '@/composables/useSeoCheck'
|
||||
import type { SeoCheckStatus, SeoFocus, SeoSuggestion, SuggestionField } from '@/lib/seoTypes'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
remembered: boolean | null
|
||||
address: string
|
||||
phase: SeoCheckPhase
|
||||
status: SeoCheckStatus | null
|
||||
suggestions: SeoSuggestion[]
|
||||
sheet?: boolean
|
||||
}>(),
|
||||
{ sheet: false },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:remembered': [open: boolean]
|
||||
edit: [focus?: SeoFocus]
|
||||
preview: []
|
||||
retry: []
|
||||
go: [field: SuggestionField]
|
||||
}>()
|
||||
|
||||
const bodyId = useId()
|
||||
|
||||
const isOpen = computed(() => props.remembered ?? false)
|
||||
const checking = computed(() => isChecking(props.phase))
|
||||
const hidden = computed(() => !checking.value && props.status === 'hidden')
|
||||
const summary = computed(() => {
|
||||
if (props.phase === 'failed') {
|
||||
return t("Couldn't check this article.")
|
||||
}
|
||||
if (checking.value) {
|
||||
return t('Checking…')
|
||||
}
|
||||
if (props.status === 'hidden') {
|
||||
return t('Hidden from search')
|
||||
}
|
||||
if (props.suggestions.length === 1) {
|
||||
return t('1 suggestion')
|
||||
}
|
||||
if (props.suggestions.length > 1) {
|
||||
return t('{0} suggestions', [props.suggestions.length])
|
||||
}
|
||||
return t('Ready')
|
||||
})
|
||||
const warning = computed(
|
||||
() => !checking.value && props.status !== 'hidden' && props.suggestions.length > 0,
|
||||
)
|
||||
const buttonClass = computed(
|
||||
() =>
|
||||
`${props.sheet ? 'h-11' : 'h-8'} w-full justify-center gap-1.5 border border-accented bg-default px-3 text-sm text-highlighted ring-0`,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col gap-3 border-b border-default pb-4" data-testid="seo-section">
|
||||
<UCollapsible :open="isOpen" @update:open="emit('update:remembered', $event)">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="link"
|
||||
class="min-h-6 w-full justify-start gap-2 p-0 text-sm font-semibold text-highlighted"
|
||||
:aria-controls="bodyId"
|
||||
data-testid="seo-toggle"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-chevron-down"
|
||||
class="size-3.5 shrink-0 transition-transform"
|
||||
:class="{ '-rotate-90': !isOpen }"
|
||||
/>
|
||||
{{ t('Search and sharing') }}
|
||||
</UButton>
|
||||
<template #content>
|
||||
<div :id="bodyId" class="mt-3 flex flex-col gap-3" data-testid="seo-body">
|
||||
<div class="flex flex-col gap-1 bg-elevated p-2" data-testid="seo-address">
|
||||
<span class="text-xs text-muted">{{ t('Address') }}</span>
|
||||
<span class="font-mono text-xs/4.5 break-all text-highlighted">{{
|
||||
address
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="hidden" class="flex gap-2 text-warning" data-testid="seo-hidden">
|
||||
<UIcon name="i-lucide-eye-off" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<span class="text-pretty text-toned">{{
|
||||
t('Hidden from search engines. This only sets a flag in the API.')
|
||||
}}</span>
|
||||
</div>
|
||||
<SuggestionList
|
||||
compact
|
||||
:phase="phase"
|
||||
:suggestions="suggestions"
|
||||
@go="emit('go', $event)"
|
||||
@more="emit('edit', 'suggestions')"
|
||||
@retry="emit('retry')"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-search"
|
||||
:class="buttonClass"
|
||||
:ui="{ leadingIcon: 'size-4' }"
|
||||
data-testid="seo-edit"
|
||||
@click="emit('edit')"
|
||||
>
|
||||
{{ t('Edit search and sharing') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-braces"
|
||||
:class="buttonClass"
|
||||
:ui="{ leadingIcon: 'size-4' }"
|
||||
data-testid="seo-preview"
|
||||
@click="emit('preview')"
|
||||
>
|
||||
{{ t('What the frontend gets') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UCollapsible>
|
||||
<span
|
||||
v-if="!isOpen"
|
||||
class="truncate pl-5.5"
|
||||
:class="warning ? 'text-warning' : 'text-muted'"
|
||||
data-testid="seo-summary"
|
||||
:title="summary"
|
||||
>{{ summary }}</span
|
||||
>
|
||||
</section>
|
||||
</template>
|
||||
Reference in new issue
Block a user