283 lines
12 KiB
JavaScript
283 lines
12 KiB
JavaScript
/**
|
|
* DatePicker Component
|
|
*
|
|
* Beautiful mobile date picker with bottom sheet modal.
|
|
* Features quick buttons (Heute, Gestern) and calendar grid.
|
|
*/
|
|
|
|
export default {
|
|
name: 'DatePicker',
|
|
emits: ['update:modelValue', 'close'],
|
|
props: {
|
|
modelValue: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
show: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
|
|
setup(props, { emit }) {
|
|
const { ref, computed, watch } = Vue;
|
|
|
|
// Current calendar view month/year
|
|
const viewDate = ref(new Date());
|
|
|
|
// Initialize view date when opened
|
|
watch(() => props.show, (newVal) => {
|
|
if (newVal && props.modelValue) {
|
|
viewDate.value = new Date(props.modelValue);
|
|
} else if (newVal) {
|
|
viewDate.value = new Date();
|
|
}
|
|
});
|
|
|
|
// German weekday names (short)
|
|
const weekDays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
|
|
|
// German month names
|
|
const monthNames = [
|
|
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
|
|
];
|
|
|
|
const shortMonthNames = [
|
|
'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'
|
|
];
|
|
|
|
const weekDayNames = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
|
|
|
// Format date for display
|
|
const formatDisplayDate = (dateStr) => {
|
|
if (!dateStr) return 'Datum wählen';
|
|
const date = new Date(dateStr);
|
|
const dayName = weekDayNames[date.getDay()];
|
|
const day = date.getDate();
|
|
const month = shortMonthNames[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
return `${dayName}, ${day}. ${month} ${year}`;
|
|
};
|
|
|
|
// Current month/year display
|
|
const currentMonthYear = computed(() => {
|
|
const month = monthNames[viewDate.value.getMonth()];
|
|
const year = viewDate.value.getFullYear();
|
|
return `${month} ${year}`;
|
|
});
|
|
|
|
// Get calendar days for current view
|
|
const calendarDays = computed(() => {
|
|
const year = viewDate.value.getFullYear();
|
|
const month = viewDate.value.getMonth();
|
|
|
|
// First day of month
|
|
const firstDay = new Date(year, month, 1);
|
|
// Last day of month
|
|
const lastDay = new Date(year, month + 1, 0);
|
|
|
|
// Day of week for first day (0=Sun, convert to 0=Mon)
|
|
let startDay = firstDay.getDay() - 1;
|
|
if (startDay < 0) startDay = 6;
|
|
|
|
const days = [];
|
|
|
|
// Add empty slots for days before first of month
|
|
for (let i = 0; i < startDay; i++) {
|
|
days.push({ day: null, date: null });
|
|
}
|
|
|
|
// Add days of month
|
|
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
const date = new Date(year, month, d);
|
|
const dateStr = formatDateISO(date);
|
|
days.push({
|
|
day: d,
|
|
date: dateStr,
|
|
isToday: isToday(date),
|
|
isSelected: dateStr === props.modelValue
|
|
});
|
|
}
|
|
|
|
return days;
|
|
});
|
|
|
|
// Check if date is today
|
|
const isToday = (date) => {
|
|
const today = new Date();
|
|
return date.toDateString() === today.toDateString();
|
|
};
|
|
|
|
// Format date as ISO string (YYYY-MM-DD)
|
|
const formatDateISO = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
// Quick date helpers
|
|
const getToday = () => formatDateISO(new Date());
|
|
const getYesterday = () => {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() - 1);
|
|
return formatDateISO(d);
|
|
};
|
|
|
|
// Navigation
|
|
const prevMonth = () => {
|
|
viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() - 1, 1);
|
|
};
|
|
|
|
const nextMonth = () => {
|
|
viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() + 1, 1);
|
|
};
|
|
|
|
// Selection
|
|
const selectDate = (dateStr) => {
|
|
if (!dateStr) return;
|
|
emit('update:modelValue', dateStr);
|
|
emit('close');
|
|
};
|
|
|
|
const selectToday = () => selectDate(getToday());
|
|
const selectYesterday = () => selectDate(getYesterday());
|
|
|
|
const close = () => {
|
|
emit('close');
|
|
};
|
|
|
|
return {
|
|
viewDate,
|
|
weekDays,
|
|
currentMonthYear,
|
|
calendarDays,
|
|
formatDisplayDate,
|
|
prevMonth,
|
|
nextMonth,
|
|
selectDate,
|
|
selectToday,
|
|
selectYesterday,
|
|
close,
|
|
getToday,
|
|
getYesterday
|
|
};
|
|
},
|
|
|
|
template: `
|
|
<!-- Bottom Sheet Modal -->
|
|
<teleport to="body">
|
|
<transition name="fade">
|
|
<div v-if="show" class="fixed inset-0 z-50">
|
|
<!-- Backdrop -->
|
|
<div class="absolute inset-0 bg-black/50" @click="close"></div>
|
|
|
|
<!-- Sheet -->
|
|
<transition name="slide-up-sheet">
|
|
<div v-if="show" class="absolute bottom-0 left-0 right-0 bg-white dark:bg-slate-800 rounded-t-2xl shadow-xl max-h-[80vh] overflow-hidden">
|
|
<!-- Handle -->
|
|
<div class="flex justify-center pt-2 pb-1">
|
|
<div class="w-10 h-1 bg-slate-300 dark:bg-slate-600 rounded-full"></div>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="px-4 pb-3 border-b border-slate-100 dark:border-slate-700">
|
|
<h3 class="text-lg font-semibold text-slate-800 dark:text-white text-center">
|
|
Datum wählen
|
|
</h3>
|
|
</div>
|
|
|
|
<!-- Quick Buttons -->
|
|
<div class="px-4 py-3 flex gap-2">
|
|
<button
|
|
@click="selectToday"
|
|
:class="[
|
|
'flex-1 py-2.5 rounded-xl font-medium transition',
|
|
modelValue === getToday()
|
|
? 'bg-primary text-white'
|
|
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
|
|
]"
|
|
>
|
|
Heute
|
|
</button>
|
|
<button
|
|
@click="selectYesterday"
|
|
:class="[
|
|
'flex-1 py-2.5 rounded-xl font-medium transition',
|
|
modelValue === getYesterday()
|
|
? 'bg-primary text-white'
|
|
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
|
|
]"
|
|
>
|
|
Gestern
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Month Navigation -->
|
|
<div class="px-4 py-2 flex items-center justify-between">
|
|
<button
|
|
@click="prevMonth"
|
|
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-400"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<span class="text-base font-semibold text-slate-800 dark:text-white">
|
|
{{ currentMonthYear }}
|
|
</span>
|
|
<button
|
|
@click="nextMonth"
|
|
class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-600 dark:text-slate-400"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Calendar Grid -->
|
|
<div class="px-4 pb-6">
|
|
<!-- Weekday Headers -->
|
|
<div class="grid grid-cols-7 gap-1 mb-2">
|
|
<div
|
|
v-for="day in weekDays"
|
|
:key="day"
|
|
class="h-8 flex items-center justify-center text-xs font-medium text-slate-500 dark:text-slate-400"
|
|
>
|
|
{{ day }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Days Grid -->
|
|
<div class="grid grid-cols-7 gap-1">
|
|
<button
|
|
v-for="(item, idx) in calendarDays"
|
|
:key="idx"
|
|
@click="selectDate(item.date)"
|
|
:disabled="!item.day"
|
|
:class="[
|
|
'h-10 flex items-center justify-center rounded-full text-sm font-medium transition',
|
|
!item.day ? 'invisible' : '',
|
|
item.isSelected ? 'bg-primary text-white' : '',
|
|
item.isToday && !item.isSelected ? 'ring-2 ring-primary text-primary' : '',
|
|
!item.isSelected && !item.isToday && item.day ? 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700' : ''
|
|
]"
|
|
>
|
|
{{ item.day }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Safe area padding for iOS -->
|
|
<div class="h-6"></div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</transition>
|
|
</teleport>
|
|
`
|
|
};
|