Xinon mobile/improve
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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>
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user