Initial commit: monorepo Naturcalabacera reservas (apps/api + apps/web + packages/shared)
This commit is contained in:
155
apps/web/src/components/CustomMobileCalendar.tsx
Normal file
155
apps/web/src/components/CustomMobileCalendar.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
format, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
||||
eachDayOfInterval, isSameMonth, isSameDay, addMonths, subMonths,
|
||||
isWithinInterval, parseISO, isBefore
|
||||
} from 'date-fns';
|
||||
import { es } from 'date-fns/locale/es';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { Reservation } from '../types';
|
||||
|
||||
interface Props {
|
||||
reservations: Reservation[];
|
||||
onSelectRange: (start: Date, end: Date) => void;
|
||||
onSelectReservation: (reservation: Reservation) => void;
|
||||
}
|
||||
|
||||
export function CustomMobileCalendar({ reservations, onSelectRange, onSelectReservation }: Props) {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectionStart, setSelectionStart] = useState<Date | null>(null);
|
||||
|
||||
const monthStart = startOfMonth(currentDate);
|
||||
const monthEnd = endOfMonth(monthStart);
|
||||
const startDate = startOfWeek(monthStart, { locale: es });
|
||||
const endDate = endOfWeek(monthEnd, { locale: es });
|
||||
|
||||
const days = eachDayOfInterval({ start: startDate, end: endDate });
|
||||
|
||||
const nextMonth = () => setCurrentDate(addMonths(currentDate, 1));
|
||||
const prevMonth = () => setCurrentDate(subMonths(currentDate, 1));
|
||||
|
||||
const getReservationForDay = (day: Date) => {
|
||||
return reservations.find(res =>
|
||||
isWithinInterval(day, { start: parseISO(res.start_date), end: parseISO(res.end_date) })
|
||||
);
|
||||
};
|
||||
|
||||
const rangeHasOverlap = (start: Date, end: Date) => {
|
||||
const rangeDays = eachDayOfInterval({ start, end });
|
||||
return rangeDays.some(day => getReservationForDay(day));
|
||||
};
|
||||
|
||||
const handleDayClick = (day: Date) => {
|
||||
const existingRes = getReservationForDay(day);
|
||||
|
||||
if (existingRes) {
|
||||
onSelectReservation(existingRes);
|
||||
setSelectionStart(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectionStart) {
|
||||
setSelectionStart(day);
|
||||
return;
|
||||
}
|
||||
|
||||
let start = selectionStart;
|
||||
let end = day;
|
||||
|
||||
if (isBefore(day, selectionStart)) {
|
||||
start = day;
|
||||
end = selectionStart;
|
||||
}
|
||||
|
||||
if (rangeHasOverlap(start, end)) {
|
||||
toast.error("No puedes seleccionar un rango que incluya días ya reservados.");
|
||||
setSelectionStart(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectRange(start, end);
|
||||
setSelectionStart(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-3xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 bg-white border-b border-gray-100">
|
||||
<h2 className="text-lg font-bold text-gray-900 capitalize">
|
||||
{format(currentDate, 'MMMM yyyy', { locale: es })}
|
||||
</h2>
|
||||
<div className="flex space-x-1">
|
||||
<button onClick={prevMonth} className="p-2 rounded-lg hover:bg-gray-50">
|
||||
<ChevronLeft size={20} className="text-gray-600" />
|
||||
</button>
|
||||
<button onClick={nextMonth} className="p-2 rounded-lg hover:bg-gray-50">
|
||||
<ChevronRight size={20} className="text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Week Days */}
|
||||
<div className="grid grid-cols-7 px-3 py-2 bg-gray-50">
|
||||
{['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'].map(day => (
|
||||
<div key={day} className="text-center text-[11px] font-semibold text-gray-500 uppercase tracking-wide py-1">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Days Grid - NUEVO DISEÑO TIPO AIRBNB */}
|
||||
<div className="grid grid-cols-7 gap-0 p-3 bg-white">
|
||||
{days.map((day) => {
|
||||
const dayRes = getReservationForDay(day);
|
||||
const isSelected = selectionStart && isSameDay(day, selectionStart);
|
||||
const isCurrentMonth = isSameMonth(day, monthStart);
|
||||
|
||||
// Palette de colores según referencia
|
||||
let bgColor = 'bg-white';
|
||||
let numberBgColor = '';
|
||||
let numberTextColor = 'text-gray-400';
|
||||
|
||||
if (dayRes) {
|
||||
if (dayRes.origin === 'Teneriffa2000') {
|
||||
bgColor = 'bg-blue-100'; // Fondo azul pastel
|
||||
numberBgColor = 'bg-blue-500'; // Círculo azul intenso
|
||||
numberTextColor = 'text-white';
|
||||
} else {
|
||||
bgColor = 'bg-yellow-100'; // Fondo amarillo pastel
|
||||
numberBgColor = 'bg-yellow-500'; // Círculo amarillo intenso
|
||||
numberTextColor = 'text-white';
|
||||
}
|
||||
} else if (isSelected) {
|
||||
bgColor = 'bg-gray-800';
|
||||
numberBgColor = 'bg-white';
|
||||
numberTextColor = 'text-gray-800';
|
||||
} else if (isCurrentMonth) {
|
||||
numberTextColor = 'text-gray-900';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={day.toString()}
|
||||
onClick={() => handleDayClick(day)}
|
||||
className={`
|
||||
relative h-12 flex items-center justify-center cursor-pointer
|
||||
transition-all duration-150
|
||||
${bgColor}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center
|
||||
text-sm font-semibold
|
||||
${numberBgColor}
|
||||
${numberTextColor}
|
||||
`}>
|
||||
{format(day, 'd')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user