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

112 lines
3.3 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed } from 'vue'
import DirectoryField from '@/components/DirectoryField.vue'
import {
MAX_SOCIAL_LINKS,
SOCIAL_PLACEHOLDERS,
SOCIAL_TYPES,
type SocialLinkRow,
type SocialType,
} from '@/lib/socialLinks'
defineProps<{
errors: Record<number, string>
disabled?: boolean
}>()
const emit = defineEmits<{ blur: [key: number] }>()
const rows = defineModel<SocialLinkRow[]>({ default: () => [] })
const typeItems = computed(() => [
{ label: 'X', value: 'x' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'GitHub', value: 'github' },
{ label: 'Mastodon', value: 'mastodon' },
{ label: t('Custom'), value: 'custom' },
])
const full = computed(() => rows.value.length >= MAX_SOCIAL_LINKS)
const counter = computed(() => t('{0} of {1}', [rows.value.length, MAX_SOCIAL_LINKS]))
function nextKey() {
return rows.value.reduce((highest, row) => Math.max(highest, row.key), 0) + 1
}
function add() {
if (full.value) {
return
}
rows.value = [...rows.value, { key: nextKey(), type: SOCIAL_TYPES[0], url: '' }]
}
function remove(key: number) {
rows.value = rows.value.filter((row) => row.key !== key)
}
function update(key: number, patch: Partial<SocialLinkRow>) {
rows.value = rows.value.map((row) => (row.key === key ? { ...row, ...patch } : row))
}
</script>
<template>
<div class="flex flex-col gap-2">
<span class="font-medium text-highlighted"
>{{ t('Social links') }}
<span class="font-normal text-muted"> {{ t('(optional)') }}</span></span
>
<DirectoryField v-for="(row, index) in rows" :key="row.key" :error="errors[row.key]">
<div class="flex items-start gap-2" data-testid="social-link-row">
<USelect
:model-value="row.type"
:items="typeItems"
:disabled="disabled"
:aria-label="t('Link type {0}', [index + 1])"
class="w-27 shrink-0 max-md:w-28"
:ui="{ base: 'h-11 text-lg md:h-10 md:text-sm' }"
@update:model-value="update(row.key, { type: $event as SocialType })"
/>
<UInput
:model-value="row.url"
:placeholder="SOCIAL_PLACEHOLDERS[row.type]"
:aria-label="t('Link URL {0}', [index + 1])"
:color="errors[row.key] ? 'error' : undefined"
:disabled="disabled"
class="min-w-0 flex-1"
:ui="{ base: 'h-11 font-mono text-base md:h-10 md:text-code' }"
@update:model-value="update(row.key, { url: String($event) })"
@blur="emit('blur', row.key)"
/>
<UButton
icon="i-lucide-trash-2"
color="neutral"
variant="outline"
:disabled="disabled"
:aria-label="t('Remove link {0}', [index + 1])"
class="size-11 shrink-0 justify-center border border-accented bg-default text-muted ring-0 md:size-10"
@click="remove(row.key)"
/>
</div>
</DirectoryField>
<div class="flex items-center gap-3">
<UButton
color="neutral"
variant="outline"
icon="i-lucide-plus"
:disabled="disabled || full"
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-7"
:ui="{ leadingIcon: 'size-3.5' }"
@click="add"
>
{{ t('Add link') }}
</UButton>
<span v-if="full" class="text-xs text-muted">{{
t('You can add up to {0} links.', [MAX_SOCIAL_LINKS])
}}</span>
<span class="text-xs text-muted">{{ counter }}</span>
</div>
</div>
</template>