feat: site languages, setup and site URL settings
This commit is contained in:
1 parent
0806733d5c
commit
d9158025d4
106 files changed
+10184
-56
No files matched your search
@@ -0,0 +1,176 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user