Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions app/components/Base/Field.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,58 @@
<script setup lang="ts">
import type { CharacterTypes } from '~/utils/iptc-iim/types'
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'

const props = defineProps<{
title: string
hasChanged: boolean
title?: string
required?: boolean
allowedCharacters?: CharacterTypes[]
}>()

const emit = defineEmits<{
(e: 'reset'): void
}>()

const value = defineModel<string>()
const field = defineModel<IPTCFieldWithValue>({ required: true })

const isValueValid = computed(() => {
if (!props.allowedCharacters || !value.value) {
if (!field.value.allowedCharacterTypes || !field.value.value) {
return true
}

return isValid(value.value, props.allowedCharacters)
return isValid(field.value.value, field.value.allowedCharacterTypes)
})

const errorMessage = computed(() => {
if (isValueValid.value) {
return undefined
if (!isValueValid.value) {
return `Only the following characters are allowed: ${field.value.allowedCharacterTypes?.join(', ')}`
}

return `Only the following characters are allowed: ${props.allowedCharacters?.join(', ')}`
if (props.required && !field.value.value) {
return 'This field is required'
}

return undefined
})

const formattedTitle = computed(() => {
return field.value.title.charAt(0).toUpperCase() + field.value.title.slice(1)
})
</script>

<template>
<UFormField :name="title" :required="required" :error="errorMessage" class="FormField w-full">
<UFormField :name="title ?? formattedTitle" :error="errorMessage" class="FormField w-full">
<template #label>
<div class="flex items-center w-full h-7 gap-1">
<span>{{ title }}</span>
<UButton v-if="hasChanged" size="sm" color="secondary" variant="link" icon="i-lucide-timer-reset" @click="emit('reset')" />
<span>{{ title ?? formattedTitle }}</span>
<UButton v-if="hasChanged" size="sm" color="secondary" variant="link" icon="i-lucide-timer-reset" @click.prevent="emit('reset')" />
</div>
</template>
<slot name="default" :error="errorMessage" />

<template #error="{ error }">
<div v-if="error" class="text-xs text-negative mt-1">
{{ error }}
</div>
</template>
<slot />
</UFormField>
</template>

Expand Down
19 changes: 17 additions & 2 deletions app/components/Base/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ withDefaults(defineProps<{
icon?: string
disabled?: boolean
clearable?: boolean
virtualize?: boolean
}>(), {
clearable: true,
})
Expand All @@ -27,16 +28,22 @@ function clear() {
:disabled="disabled"
:color="hasChanged ? 'secondary' : undefined"
:highlight="hasChanged"
class="w-full"
class="BaseSelectMenuParentClass w-full"
:ui="{
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-100 hover:cursor-pointer',
}"
:items="options"
:icon="icon"
:virtualize="virtualize"
>
<template #leading="{ modelValue, ui }">
<Icon v-if="icon" :name="icon" :class="ui.leadingIcon()" />
<slot class="test" name="leading" :model-value="modelValue" :ui="ui" />
</template>

<template #default>
<div class="flex items-center justify-between w-full">
<slot v-if="value && value.label" name="label" />
<slot v-if="value && value.label" name="label" :model-value="value" />
<span v-else-if="placeholder" class="text-default/50">{{ placeholder }}</span>
<slot v-else name="placeholder" />
<UButton
Expand All @@ -56,3 +63,11 @@ function clear() {
</template>
</USelectMenu>
</template>

<style>
@reference "tailwindcss";

.BaseSelectMenuParentClass:has(> span:empty) {
@apply ps-2.5;
}
</style>
50 changes: 49 additions & 1 deletion app/components/Editor/Categories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,51 @@ const fieldsByKey = computed({
})
},
})

function getExtraFields(key: string): (IPTCFieldWithValue & { type: 'extra' })[] {
if (!fieldsByKey.value || !fieldsByKey.value[key]) {
return []
}

const field = fieldsByKey.value[key]

if (field.type === 'location') {
const nameField = fieldsByKey.value[field.nameKey]
return nameField && isFieldType('extra', nameField) ? [nameField] : []
}

if (field.type === 'reference') {
const dateField = fieldsByKey.value[field.dateKey]
const numberField = fieldsByKey.value[field.numberKey]
const extraFields: (IPTCFieldWithValue & { type: 'extra' })[] = []

if (dateField && isFieldType('extra', dateField)) {
extraFields.push(dateField)
}

if (numberField && isFieldType('extra', numberField)) {
extraFields.push(numberField)
}

return extraFields
}

return []
}

function updateExtraField(updatedFields: (IPTCFieldWithValue & { type: 'extra' })[]) {
if (!fieldsByKey.value) {
return
}

fieldsByKey.value = {
...fieldsByKey.value,
...updatedFields.reduce((acc, field) => {
acc[field.key] = field
return acc
}, {} as Record<string, IPTCFieldWithValue>),
}
}
</script>

<template>
Expand All @@ -43,8 +88,11 @@ const fieldsByKey = computed({
<div v-for="(row, index) in category.rows" :key="row.join(':')" class="flex flex-col sm:flex-row items-center gap-2 w-full">
<template v-for="{ key, width } in category.rows[index]" :key="`${index}-${key}`">
<EditorField
v-if="fieldsByKey[key]" v-model="fieldsByKey[key].value" :field="fieldsByKey[key]"
v-if="fieldsByKey[key]"
v-model="fieldsByKey[key]"
:extra="getExtraFields(key)"
:style="width ? `width: ${width}%` : ''"
@update:extra="updateExtraField"
/>
</template>
</div>
Expand Down
36 changes: 19 additions & 17 deletions app/components/Editor/Field.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
<script setup lang="ts">
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'

const props = defineProps<{
field: IPTCFieldWithValue
placeholder?: string
}>()
const field = defineModel<IPTCFieldWithValue>({ required: true })

const value = defineModel<string>()

const formattedTitle = computed(() => {
return props.field.title.charAt(0).toUpperCase() + props.field.title.slice(1)
})
const extra = defineModel<(IPTCFieldWithValue & { type: 'extra' })[]>('extra', { required: true })
</script>

<template>
<EditorFieldDate v-if="['date', 'datetime'].includes(field.type)" v-model="value" :title="formattedTitle" :placeholder="placeholder" :original="field.original" />
<EditorFieldNumber v-else-if="field.type === 'number'" v-model="value" :title="formattedTitle" :placeholder="placeholder" :original="field.original" :min="field.minValue" :max="field.maxValue" />
<EditorFieldTime v-else-if="field.type === 'time'" v-model="value" :title="formattedTitle" :placeholder="placeholder" :original="field.original" />
<EditorFieldSelect v-else-if="field.type === 'select'" v-model="value" :title="formattedTitle" :placeholder="placeholder" :options="field.options" :original="field.original" />
<EditorFieldObjectTypeOrAttribute v-else-if="field.type === 'object-type' || field.type === 'object-attribute'" v-model="value" :title="formattedTitle" :type="field.type" :original="field.original" />
<EditorFieldSubject v-else-if="field.type === 'subject-reference'" v-model="value" :original="field.original" />
<EditorFieldTextArea v-else-if="field.type === 'textarea'" v-model="value" :title="formattedTitle" :placeholder="placeholder" :original="field.original" :octets="field.octets" :allowed-characters="field.allowedCharacterTypes" />
<EditorFieldText v-else-if="field.type === 'text'" v-model="value" :title="formattedTitle" :placeholder="placeholder" :original="field.original" :octets="field.octets" :allowed-characters="field.allowedCharacterTypes" />
<EditorFieldDate v-if="isFieldType('date', field)" v-model="field" />
<EditorFieldNumber v-else-if="isFieldType('number', field)" v-model="field" />
<EditorFieldTime v-else-if="isFieldType('time', field)" v-model="field" />
<EditorFieldSelect v-else-if="isFieldType('select', field)" v-model="field" />
<EditorFieldObjectTypeOrAttribute v-else-if="isFieldType('object-type', field) || isFieldType('object-attribute', field)" v-model="field" />
<EditorFieldSubject v-else-if="isFieldType('subject-reference', field)" v-model="field" />
<EditorFieldTextArea v-else-if="isFieldType('textarea', field)" v-model="field" />
<EditorFieldText v-else-if="isFieldType('text', field)" v-model="field" />
<EditorFieldSlider v-else-if="isFieldType('slider', field)" v-model="field" />
<EditorFieldLocation v-else-if="isFieldType('location', field) && extra[0] && isFieldType('extra', extra[0])" v-model:code="field" v-model:name="extra[0]" />
<EditorFieldReference
v-else-if="isFieldType('reference', field) && extra[0] && extra[1] && isFieldType('extra', extra[0]) && isFieldType('extra', extra[1])"
v-model="field"
v-model:date="extra[0]"
v-model:number="extra[1]"
/>
<EditorFieldLanguage v-else-if="isFieldType('language', field)" v-model="field" />
</template>
67 changes: 34 additions & 33 deletions app/components/Editor/Field/Date.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
<script setup lang="ts">
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'
import { CalendarDate } from '@internationalized/date'

const props = withDefaults(defineProps<{
title: string
placeholder?: string
icon?: string
defineProps<{
disabled?: boolean
required?: boolean
original: string
}>(), {
placeholder: 'Select a date',
})
}>()

const value = defineModel<string>()
const field = defineModel<IPTCFieldWithValue & { type: 'date' | 'extra' }>({ required: true })

const dateValue = ref<CalendarDate | undefined>()

Expand All @@ -27,13 +23,16 @@ function parseDate(value?: string): CalendarDate | undefined {
return undefined
}

watch(value, (newValue) => {
if (newValue) {
const date = parseDate(newValue)
watch(() => field.value.value, (newValue) => {
if (!newValue) {
dateValue.value = undefined
return
}

const date = parseDate(newValue)

if (!dateValue.value || (date && !date.compare(dateValue.value))) {
dateValue.value = date
}
if (!dateValue.value || (date && !date.compare(dateValue.value))) {
dateValue.value = date
}
}, { immediate: true })

Expand All @@ -45,32 +44,34 @@ function updateDate(newDate?: CalendarDate) {
const day = newDate.day.toString().padStart(2, '0')
const year = newDate.year.toString().padStart(4, '0')

value.value = `${year}${month}${day}`
field.value.value = `${year}${month}${day}`
}
else {
value.value = ''
field.value.value = ''
}
}
const original = computed(() => props.original)
const hasChanged = useHasChanged(original, value)

const hasChanged = useHasChanged(field)
</script>

<template>
<BaseField v-model="value" :title="title" :required="required" :has-changed="hasChanged" @reset="updateDate(parseDate(original))">
<UPopover arrow :content="{ side: 'top' }">
<UButton :color="hasChanged ? 'secondary' : 'neutral'" variant="subtle" icon="i-lucide-calendar" class="w-full h-8">
<template #trailing>
<UButton v-if="dateValue" icon="i-lucide-circle-x" variant="link" :color="hasChanged ? 'secondary' : 'neutral'" size="sm" @click.stop="updateDate(undefined)" />
</template>
<BaseField v-model="field" :has-changed="hasChanged" :required="required" @reset="updateDate(parseDate(field.original))">
<template #default="{ error }">
<UPopover arrow :content="{ side: 'top' }">
<UButton :color="error ? 'error' : hasChanged ? 'secondary' : 'neutral'" variant="outline" :icon="field.icon ?? 'i-lucide-calendar'" :disabled="disabled" class="w-full h-8">
<template #trailing>
<UButton v-if="dateValue" icon="i-lucide-circle-x" variant="link" :color="hasChanged ? 'secondary' : 'neutral'" size="sm" @click.stop="updateDate(undefined)" />
</template>

<div class="w-full text-start">
{{ dateValue ? formatDate(dateValue.toString(), 'DD MMMM YYYY') : placeholder }}
</div>
</UButton>
<div class="w-full text-start">
{{ dateValue ? formatDate(dateValue.toString(), 'DD MMMM YYYY') : (field.placeholder || 'Select a date') }}
</div>
</UButton>

<template #content>
<UCalendar :model-value="dateValue" class="p-2" @update:model-value="$event => $event ? updateDate($event as CalendarDate) : undefined" />
</template>
</UPopover>
<template #content>
<UCalendar :model-value="dateValue" class="p-2" @update:model-value="$event => $event ? updateDate($event as CalendarDate) : undefined" />
</template>
</UPopover>
</template>
</BaseField>
</template>
41 changes: 41 additions & 0 deletions app/components/Editor/Field/Language.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { IPTCFieldWithValue } from '~/utils/iptc-iim/types'

const field = defineModel<IPTCFieldWithValue & { type: 'language' }>({ required: true })

const hasChanged = useHasChanged(field)

const selectedLanguage = computed({
get: () => languages.find(lang => lang.value === field.value.value),
set: (newValue) => {
field.value.value = newValue ? newValue.value : ''
},
})
</script>

<template>
<BaseField v-model="field" :has-changed="hasChanged" @reset="field.value = field.original">
<BaseSelect
v-model="selectedLanguage"
:options="languages"
:has-changed="hasChanged"
:placeholder="field.placeholder ?? 'Select a language'"
virtualize
>
<template #leading="{ modelValue, ui }">
<span v-if="modelValue?.value" class="size-5 text-default/75 text-center">
{{ modelValue.value }}
</span>
<UIcon v-else name="i-lucide-languages" :class="ui.leadingIcon()" />
</template>

<template #label="{ modelValue }">
{{ modelValue.label }}
</template>

<template #item-leading="{ item }">
<span class="size-5 text-default/75 text-center">{{ item.value }}</span>
</template>
</BaseSelect>
</BaseField>
</template>
Loading