112 lines
3.3 KiB
Vue
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>
|