Initial commit: monorepo Naturcalabacera reservas (apps/api + apps/web + packages/shared)

This commit is contained in:
2026-04-30 10:09:44 +01:00
commit a0ccb8ca64
188 changed files with 16418 additions and 0 deletions

View 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>
);
}