'use client'; import { useEffect, useState, useRef } from 'react'; import { formatDisplayDate } from '@/lib/date'; import RoomCalendar from '@/components/RoomCalendar'; import OptimizedImage from '@/components/OptimizedImage'; interface Room { id: number; name: string; description: string; price: string; photos: string[]; featured_photo: string | null; active?: boolean; amenities: Array<{ id: number; name: string; icon_svg: string }>; } interface RoomComment { id: number; room_id: number; photo_url: string | null; user_id: number; first_name: string; last_initial: string | null; text: string; created_at: string; } interface User { id: number; username: string; role: 'admin' | 'user'; first_name: string | null; last_name: string | null; email?: string; comments_disabled: boolean; } interface RoomLike { room_id: number; user_id: number; created_at: string; } const gold = '#E8A849'; const navy = '#010D1E'; const cream = '#f5f0e8'; const warm = '#a6683c'; export default function RoomsPage() { const [rooms, setRooms] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [user, setUser] = useState(null); const [commentsByRoom, setCommentsByRoom] = useState>({}); const [likesByRoom, setLikesByRoom] = useState>({}); const [userLikesByRoom, setUserLikesByRoom] = useState>({}); const [expandedRoom, setExpandedRoom] = useState(null); const [activePhotoByRoom, setActivePhotoByRoom] = useState>({}); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [loadingComments, setLoadingComments] = useState>({}); // Action modal states const [activeModal, setActiveModal] = useState<'reserve' | 'message' | 'calendar' | 'review' | null>(null); const [selectedRoom, setSelectedRoom] = useState(null); const [reservationDates, setReservationDates] = useState({ checkIn: '', checkOut: '' }); const [messageText, setMessageText] = useState(''); const [reviewText, setReviewText] = useState(''); const [submitting, setSubmitting] = useState(false); const [guestForm, setGuestForm] = useState({ name: '', contactMethod: 'email', contact: '' }); useEffect(() => { fetch('/api/rooms') .then(async (res) => { if (!res.ok) throw new Error('Failed to load rooms'); const json = await res.json(); setRooms(json.data || []); json.data?.forEach((r: Room) => { loadComments(r.id); loadLikes(r.id); }); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); fetch('/api/auth/me', { credentials: 'same-origin' }) .then((r) => r.json()) .then((j) => setUser(j.user || null)) .catch(() => {}); }, []); const loadComments = async (roomId: number) => { try { setLoadingComments((prev) => ({ ...prev, [roomId]: true })); const res = await fetch(`/api/rooms/${roomId}/comments`, { credentials: 'same-origin' }); const json = await res.json().catch(() => ({ data: [] })); setCommentsByRoom((prev) => ({ ...prev, [roomId]: json.data || [] })); } catch { /* ignore */ } finally { setLoadingComments((prev) => ({ ...prev, [roomId]: false })); } }; const loadLikes = async (roomId: number) => { try { const res = await fetch(`/api/rooms/${roomId}/likes`, { credentials: 'same-origin' }); const json = await res.json().catch(() => ({ count: 0 })); setLikesByRoom((prev) => ({ ...prev, [roomId]: json.count || 0 })); setUserLikesByRoom((prev) => ({ ...prev, [roomId]: !!json.user_liked })); } catch { /* ignore */ } }; const postComment = async (roomId: number) => { if (!draft.trim() || sending) return; setSending(true); const room = rooms.find(r => r.id === roomId); if (!room) return; const photo = getPhoto(room, activePhotoByRoom[roomId] || 0); await fetch(`/api/rooms/${roomId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ text: draft.trim(), photo_url: photo }), }).catch(() => {}); setDraft(''); await loadComments(roomId); setSending(false); }; const deleteComment = async (roomId: number, commentId: number) => { await fetch(`/api/admin/comments/${commentId}`, { method: 'DELETE', credentials: 'same-origin' }); await loadComments(roomId); }; const toggleLike = async (roomId: number) => { if (!user) { alert('Log in to like rooms'); return; } try { const res = await fetch(`/api/rooms/${roomId}/likes`, { method: 'POST', credentials: 'same-origin', }); if (res.ok) { const json = await res.json().catch(() => ({})); setLikesByRoom((prev) => ({ ...prev, [roomId]: json.count || (likesByRoom[roomId] || 0) + (userLikesByRoom[roomId] ? -1 : 1) })); setUserLikesByRoom((prev) => ({ ...prev, [roomId]: !userLikesByRoom[roomId] })); } } catch { /* ignore */ } }; const getPhoto = (room: Room, index: number): string | null => { if (!room.photos || room.photos.length === 0) return null; const photo = room.photos[Math.min(index, room.photos.length - 1)]; if (!photo) return null; // If it's a URL with id= param, extract id; if it's base64, return as-is if (photo.includes('id=')) { return photo; } return photo; }; const getPhotoId = (photo: string): number | null => { if (!photo) return null; if (photo.includes('id=')) { const match = photo.match(/id=(\d+)/); return match ? parseInt(match[1]) : null; } return null; }; const getActiveIndex = (room: Room): number => { return activePhotoByRoom[room.id] ?? 0; }; const openModal = (type: 'reserve' | 'message' | 'calendar' | 'review', room: Room) => { setSelectedRoom(room); setActiveModal(type); setReservationDates({ checkIn: '', checkOut: '' }); setMessageText(''); setReviewText(''); }; const closeModal = () => { setActiveModal(null); setSelectedRoom(null); }; const handleReserve = async () => { if (!selectedRoom || !reservationDates.checkIn || !reservationDates.checkOut) { alert('Please select dates'); return; } // For guests, require name and contact if (!user && (!guestForm.name.trim() || !guestForm.contact.trim())) { alert('Please provide your name and contact info'); return; } setSubmitting(true); try { const res = await fetch('/api/reservations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ room_id: selectedRoom.id, check_in: reservationDates.checkIn, check_out: reservationDates.checkOut, ...(user ? {} : { guest_name: guestForm.name.trim(), guest_contact_method: guestForm.contactMethod, guest_contact: guestForm.contact.trim(), }), }), }); if (res.ok) { alert('Reservation request sent! You will receive a confirmation shortly.'); closeModal(); setReservationDates({ checkIn: '', checkOut: '' }); setGuestForm({ name: '', contactMethod: 'email', contact: '' }); } else { const err = await res.json(); alert(err.error || 'Failed to create reservation'); } } catch { alert('Failed to create reservation'); } setSubmitting(false); }; const handleSendMessage = async () => { if (!selectedRoom || !messageText.trim()) { alert('Please enter a message'); return; } // For guests, require name and contact if (!user && (!guestForm.name.trim() || !guestForm.contact.trim())) { alert('Please provide your name and contact info'); return; } setSubmitting(true); try { const res = await fetch('/api/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ room_id: selectedRoom.id, message: messageText.trim(), ...(user ? {} : { guest_name: guestForm.name.trim(), guest_contact_method: guestForm.contactMethod, guest_contact: guestForm.contact.trim(), }), }), }); if (res.ok) { alert('Message sent to our team!'); closeModal(); setMessageText(''); setGuestForm({ name: '', contactMethod: 'email', contact: '' }); } else { const err = await res.json(); alert(err.error || 'Failed to send message'); } } catch { alert('Failed to send message'); } setSubmitting(false); }; const handleReview = async () => { if (!selectedRoom || !reviewText.trim() || !user) { alert('Please log in and enter a review'); return; } setSubmitting(true); try { const res = await fetch(`/api/rooms/${selectedRoom.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ text: reviewText.trim(), status: 'pending' }), }); if (res.ok) { alert('Review submitted for approval'); closeModal(); loadComments(selectedRoom.id); } else { const err = await res.json(); alert(err.error || 'Failed to submit review'); } } catch { alert('Failed to submit review'); } setSubmitting(false); }; const displayedRooms = rooms.filter((r) => r.active !== false); return (

Rooms & Suites

Comfortable stays with views of the cloud forest.

{loading && (
{[1, 2, 3].map((i) => (
))}
)} {error &&

{error}

} {!loading && displayedRooms.length === 0 &&

No rooms available yet.

}
{displayedRooms.map((room) => { const photos = room.photos || []; const activeIdx = getActiveIndex(room); const activeSrc = getPhoto(room, activeIdx); const comments = commentsByRoom[room.id] || []; const isExpanded = expandedRoom === room.id; const totalLikes = likesByRoom[room.id] || 0; const hasLiked = userLikesByRoom[room.id] || false; return (
{ e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(1,13,30,0.14)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(1,13,30,0.08)'; }} > {/* ─── Main Image ─── */}
{activeSrc ? ( activeSrc.startsWith('data:') ? ( {room.name} ) : ( ) ) : (
No photo
)} {/* Like button with count */}
{/* ─── Thumbnail Strip ─── */} {photos.length > 1 && (
{photos.map((p, i) => { const isActive = i === activeIdx; return ( ); })}
)} {/* ─── Room Info ─── */}

{room.name}

{room.description}

{/* ─── Amenity Icons ─── */} {room.amenities && room.amenities.length > 0 && (
{room.amenities.slice(0, 6).map((amenity) => (
{amenity.icon_svg ? ( ) : null} {amenity.name}
))} {room.amenities.length > 6 && ( +{room.amenities.length - 6} )}
)}
${room.price} per night
{/* ─── Action Buttons ─── */}
{/* Comments button — cream */} {/* Comments section */} {isExpanded && (
{loadingComments[room.id] &&

Loading comments...

} {user ? ( user.comments_disabled ? (

Comments are disabled for your account.

) : (