177 lines
5.1 KiB
Vue
177 lines
5.1 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, onMounted, toRef, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import DashboardShell from '@/components/DashboardShell.vue'
|
|
import LocaleAlert from '@/components/localization/LocaleAlert.vue'
|
|
import LocaleButton from '@/components/localization/LocaleButton.vue'
|
|
import LocaleInput from '@/components/localization/LocaleInput.vue'
|
|
import RetryAlert from '@/components/RetryAlert.vue'
|
|
import { usePublicUrlPage } from '@/composables/usePublicUrlPage'
|
|
import { PUBLIC_URL_PATTERN_MAX_LENGTH } from '@/lib/publicUrlPattern'
|
|
import { usePermissionsStore } from '@/stores/permissions'
|
|
import { useSessionStore } from '@/stores/session'
|
|
|
|
const route = useRoute()
|
|
const session = useSessionStore()
|
|
const permissions = usePermissionsStore()
|
|
|
|
const siteKey = computed(() => String(route.params.key ?? ''))
|
|
const page = usePublicUrlPage(toRef(siteKey))
|
|
const { loadState, value, saving, saveFailed, unset, canSave, errorMessage } = page
|
|
|
|
const sessionReady = computed(() => session.status === 'ready')
|
|
const owner = computed(() => permissions.can('settings.manage_sites'))
|
|
const siteName = computed(() => page.site.value?.name ?? siteKey.value)
|
|
const ready = computed(() => loadState.value === 'ready')
|
|
const invalid = computed(() => errorMessage.value !== undefined)
|
|
|
|
onMounted(page.load)
|
|
watch(siteKey, page.load)
|
|
|
|
const breadcrumb = computed(() => [
|
|
{ label: t('Settings'), to: '/settings' },
|
|
{ label: t('Sites'), to: '/settings/sites' },
|
|
{ label: siteName.value },
|
|
{ label: t('Public URL') },
|
|
])
|
|
</script>
|
|
|
|
<template>
|
|
<DashboardShell
|
|
compact-padding
|
|
:mobile-title="t('{0} · Public URL', [siteName])"
|
|
back-to="/settings/sites"
|
|
:back-label="t('Back to Sites')"
|
|
no-current-nav
|
|
>
|
|
<template #breadcrumb>
|
|
<UBreadcrumb
|
|
:items="breadcrumb"
|
|
color="neutral"
|
|
class="hidden lg:block"
|
|
:ui="{ link: 'text-sm font-medium' }"
|
|
/>
|
|
</template>
|
|
|
|
<div v-if="sessionReady" class="flex flex-col gap-4">
|
|
<div class="flex min-w-0 flex-col gap-0.5">
|
|
<h1 class="text-xl font-semibold text-highlighted">{{ t('Public URL') }}</h1>
|
|
<p class="text-pretty text-toned">
|
|
{{
|
|
t(
|
|
'Where {0} articles appear on your website. Used for canonical and alternate links.',
|
|
[siteName],
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="loadState === 'loading'"
|
|
class="h-44 bg-elevated"
|
|
role="status"
|
|
:aria-label="t('Loading')"
|
|
/>
|
|
|
|
<RetryAlert
|
|
v-else-if="loadState === 'error'"
|
|
:title="t('Couldn\'t load languages')"
|
|
:description="t('Check your connection and try again.')"
|
|
@retry="page.load"
|
|
/>
|
|
|
|
<template v-else-if="ready">
|
|
<LocaleAlert
|
|
v-if="!owner"
|
|
tone="neutral"
|
|
icon="i-lucide-lock"
|
|
:title="t('Only owners can change this')"
|
|
:description="t('You can read the pattern. Ask an owner to change it.')"
|
|
/>
|
|
<LocaleAlert
|
|
v-else-if="unset"
|
|
tone="info"
|
|
icon="i-lucide-info"
|
|
:title="t('Public URL not configured')"
|
|
:description="
|
|
t(
|
|
'Page, canonical and alternate addresses stay hidden until you add a pattern. Links by article ID still work.',
|
|
)
|
|
"
|
|
/>
|
|
<LocaleAlert
|
|
v-if="saveFailed"
|
|
tone="error"
|
|
icon="i-lucide-circle-x"
|
|
:title="t('Couldn\'t save languages')"
|
|
:description="t('Nothing changed. Your selection is kept. Try again.')"
|
|
/>
|
|
|
|
<UForm
|
|
:state="{ value }"
|
|
class="flex flex-col gap-3 border border-default bg-default p-4"
|
|
@submit="page.save"
|
|
>
|
|
<UFormField
|
|
:label="t('Public URL pattern')"
|
|
:error="errorMessage"
|
|
:help="
|
|
t(
|
|
'Placeholders: {lang} is the language code, such as fr or pt-BR. {slug} is the article address in that language.',
|
|
)
|
|
"
|
|
:ui="{
|
|
label: 'font-medium text-highlighted',
|
|
container: 'mt-1.5',
|
|
help: 'mt-1.5 text-xs/4.5 text-pretty text-muted',
|
|
error: 'mt-1.5 flex gap-1.5 text-xs/4.5 text-error',
|
|
}"
|
|
>
|
|
<LocaleInput
|
|
:model-value="value"
|
|
mono
|
|
:readonly="!owner"
|
|
:invalid="invalid"
|
|
placeholder="https://example.com/{lang}/{slug}"
|
|
:aria-label="t('Public URL pattern')"
|
|
:maxlength="PUBLIC_URL_PATTERN_MAX_LENGTH"
|
|
@update:model-value="page.edit"
|
|
/>
|
|
<template v-if="errorMessage" #error>
|
|
<UIcon name="i-lucide-circle-x" class="mt-0.5 size-3.5 shrink-0" />
|
|
<span>{{ errorMessage }}</span>
|
|
</template>
|
|
</UFormField>
|
|
|
|
<div
|
|
v-if="page.previewUrl.value"
|
|
class="flex flex-col gap-1"
|
|
data-testid="preview"
|
|
>
|
|
<span class="text-xs/5 text-muted">{{ page.previewLabel.value }}</span>
|
|
<span class="font-mono text-xs/5 wrap-anywhere text-highlighted">{{
|
|
page.previewUrl.value
|
|
}}</span>
|
|
</div>
|
|
|
|
<div v-if="owner" class="flex gap-2">
|
|
<LocaleButton
|
|
type="submit"
|
|
kind="primary"
|
|
:disabled="!canSave"
|
|
:loading="saving"
|
|
>
|
|
{{ t('Save') }}
|
|
</LocaleButton>
|
|
<LocaleButton :disabled="saving" @click="page.cancel">
|
|
{{ t('Cancel') }}
|
|
</LocaleButton>
|
|
</div>
|
|
</UForm>
|
|
</template>
|
|
</div>
|
|
</DashboardShell>
|
|
</template>
|