165 lines
4.1 KiB
Vue
165 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
import { CalendarDate, parseDate } from '@internationalized/date'
|
|
import { RANGE_KEYS, rangeLabel, type RangeKey } from '@/lib/activity'
|
|
import { SEGMENT_GROUP, segmentClass } from '@/lib/segmentStyles'
|
|
|
|
interface RangeChange {
|
|
range: RangeKey
|
|
from: string
|
|
to: string
|
|
}
|
|
|
|
interface CalendarRange {
|
|
start?: CalendarDate
|
|
end?: CalendarDate
|
|
}
|
|
|
|
const props = defineProps<{
|
|
range: RangeKey
|
|
from: string
|
|
to: string
|
|
block?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
change: [value: RangeChange]
|
|
}>()
|
|
|
|
const popoverOpen = ref(false)
|
|
const customPending = ref(false)
|
|
const draftFrom = ref(props.from)
|
|
const draftTo = ref(props.to)
|
|
|
|
const presets = RANGE_KEYS.filter((key) => key !== 'custom')
|
|
const customSelected = computed(
|
|
() => props.range === 'custom' || popoverOpen.value || customPending.value,
|
|
)
|
|
const showFields = computed(() => props.block && customSelected.value)
|
|
|
|
function parse(value: string) {
|
|
try {
|
|
return parseDate(value)
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
const calendarValue = computed<CalendarRange>(() => ({
|
|
start: parse(props.from),
|
|
end: parse(props.to),
|
|
}))
|
|
|
|
watch(
|
|
() => [props.from, props.to],
|
|
([from, to]) => {
|
|
draftFrom.value = from ?? ''
|
|
draftTo.value = to ?? ''
|
|
},
|
|
)
|
|
|
|
function choose(range: RangeKey) {
|
|
customPending.value = false
|
|
emit('change', { range, from: '', to: '' })
|
|
}
|
|
|
|
function chooseCustom() {
|
|
if (props.block) customPending.value = true
|
|
}
|
|
|
|
function applyCustom(from: string, to: string) {
|
|
customPending.value = false
|
|
emit(
|
|
'change',
|
|
from <= to ? { range: 'custom', from, to } : { range: 'custom', from: to, to: from },
|
|
)
|
|
}
|
|
|
|
function onCalendar(value: CalendarRange | null) {
|
|
if (!value?.start || !value.end) return
|
|
popoverOpen.value = false
|
|
applyCustom(value.start.toString(), value.end.toString())
|
|
}
|
|
|
|
function onField(side: 'from' | 'to', value: string | number | null | undefined) {
|
|
const next = typeof value === 'string' ? value : ''
|
|
if (side === 'from') draftFrom.value = next
|
|
else draftTo.value = next
|
|
if (draftFrom.value && draftTo.value) applyCustom(draftFrom.value, draftTo.value)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-2" :class="block ? 'w-full' : ''">
|
|
<div role="group" :aria-label="t('Dates')" :class="[SEGMENT_GROUP, block ? 'w-full' : '']">
|
|
<UButton
|
|
v-for="key in presets"
|
|
:key="key"
|
|
color="neutral"
|
|
:variant="range === key && !customSelected ? 'soft' : 'outline'"
|
|
:aria-pressed="range === key && !customSelected"
|
|
:class="segmentClass(range === key && !customSelected, block)"
|
|
@click="choose(key)"
|
|
>
|
|
{{ rangeLabel(key) }}
|
|
</UButton>
|
|
<UButton
|
|
v-if="block"
|
|
color="neutral"
|
|
:variant="customSelected ? 'soft' : 'outline'"
|
|
icon="i-lucide-calendar"
|
|
:aria-pressed="customSelected"
|
|
:class="segmentClass(customSelected, block)"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="chooseCustom"
|
|
>
|
|
{{ rangeLabel('custom') }}
|
|
</UButton>
|
|
<UPopover v-else v-model:open="popoverOpen" :content="{ align: 'end' }">
|
|
<UButton
|
|
color="neutral"
|
|
:variant="customSelected ? 'soft' : 'outline'"
|
|
icon="i-lucide-calendar"
|
|
:aria-pressed="customSelected"
|
|
:class="segmentClass(customSelected)"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
>
|
|
{{ rangeLabel('custom') }}
|
|
</UButton>
|
|
<template #content>
|
|
<UCalendar
|
|
range
|
|
:model-value="calendarValue"
|
|
:number-of-months="1"
|
|
class="p-2"
|
|
@update:model-value="onCalendar($event as CalendarRange | null)"
|
|
/>
|
|
</template>
|
|
</UPopover>
|
|
</div>
|
|
|
|
<div v-if="showFields" class="flex items-center gap-2">
|
|
<UInput
|
|
type="date"
|
|
icon="i-lucide-calendar"
|
|
:model-value="draftFrom"
|
|
:aria-label="t('Start date')"
|
|
class="min-w-0 flex-1"
|
|
:ui="{ base: 'h-11 w-full min-w-0 text-lg' }"
|
|
@update:model-value="onField('from', $event)"
|
|
/>
|
|
<UInput
|
|
type="date"
|
|
icon="i-lucide-calendar"
|
|
:model-value="draftTo"
|
|
:aria-label="t('End date')"
|
|
class="min-w-0 flex-1"
|
|
:ui="{ base: 'h-11 w-full min-w-0 text-lg' }"
|
|
@update:model-value="onField('to', $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|