Files
la-huasca-ts/src/app/rooms/page.tsx
T
muken 9557fa7d07 fix: logged-in users can now comment without entering personal info
- Fix user data extraction in rooms page (was looking for j.user instead of direct fields)
- Add email to /api/auth/me response for logged-in user display
- User object now properly populated with id, username, role, first_name, last_name, email, comments_disabled
- Comments section now correctly shows 'Posting as {name}' for logged-in users
- Reservations and messages already use user info when logged in, only require guest info for guests
2026-07-03 00:14:32 -05:00

928 lines
43 KiB
TypeScript

'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<Room[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [user, setUser] = useState<User | null>(null);
const [commentsByRoom, setCommentsByRoom] = useState<Record<number, RoomComment[]>>({});
const [likesByRoom, setLikesByRoom] = useState<Record<number, number>>({});
const [userLikesByRoom, setUserLikesByRoom] = useState<Record<number, boolean>>({});
const [expandedRoom, setExpandedRoom] = useState<number | null>(null);
const [activePhotoByRoom, setActivePhotoByRoom] = useState<Record<number, number>>({});
const [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
const [loadingComments, setLoadingComments] = useState<Record<number, boolean>>({});
// Action modal states
const [activeModal, setActiveModal] = useState<'reserve' | 'message' | 'calendar' | 'review' | null>(null);
const [selectedRoom, setSelectedRoom] = useState<Room | null>(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) => {
if (j.authenticated) {
setUser({
id: j.id,
username: j.username,
role: j.role,
first_name: j.first_name,
last_name: j.last_name,
email: j.email,
comments_disabled: j.comments_disabled,
});
} else {
setUser(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 (
<main style={{ padding: '2rem', maxWidth: '80rem', margin: '0 auto', minHeight: '60vh' }}>
<h1 style={{ fontSize: 'clamp(32px, 5vw, 48px)', fontWeight: 400, fontStyle: 'italic', color: navy, margin: '0 0 0.5rem' }}>Rooms &amp; Suites</h1>
<p style={{ color: '#555', marginBottom: '2rem', maxWidth: '50ch' }}>
Comfortable stays with views of the cloud forest.
</p>
{loading && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))', gap: '2rem' }}>
{[1, 2, 3].map((i) => (
<div key={i} style={{ background: cream, borderRadius: '20px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(1,13,30,0.08)' }}>
<div style={{ width: '100%', height: '300px', background: 'linear-gradient(90deg, #e8e4dc 25%, #f5f0e8 50%, #e8e4dc 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.5s infinite' }} />
<div style={{ padding: '1.5rem' }}>
<div style={{ height: '24px', width: '60%', background: 'linear-gradient(90deg, #e8e4dc 25%, #f5f0e8 50%, #e8e4dc 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.5s infinite', borderRadius: '4px', marginBottom: '0.75rem' }} />
<div style={{ height: '16px', width: '90%', background: 'linear-gradient(90deg, #e8e4dc 25%, #f5f0e8 50%, #e8e4dc 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.5s infinite', borderRadius: '4px', marginBottom: '0.5rem' }} />
<div style={{ height: '16px', width: '75%', background: 'linear-gradient(90deg, #e8e4dc 25%, #f5f0e8 50%, #e8e4dc 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.5s infinite', borderRadius: '4px', marginBottom: '0.5rem' }} />
<div style={{ height: '16px', width: '50%', background: 'linear-gradient(90deg, #e8e4dc 25%, #f5f0e8 50%, #e8e4dc 75%)', backgroundSize: '200% 100%', animation: 'shimmer 1.5s infinite', borderRadius: '4px' }} />
</div>
</div>
))}
</div>
)}
<style jsx>{`
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`}</style>
{error && <p style={{ color: '#c23b22' }}>{error}</p>}
{!loading && displayedRooms.length === 0 && <p style={{ color: '#777' }}>No rooms available yet.</p>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(380px, 1fr))', gap: '2rem' }}>
{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 (
<article
key={room.id}
style={{
background: cream,
borderRadius: '20px',
overflow: 'hidden',
boxShadow: '0 2px 8px rgba(1,13,30,0.08)',
display: 'flex',
flexDirection: 'column',
transition: 'transform 0.2s, box-shadow 0.2s',
}}
onMouseEnter={(e) => { 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 ─── */}
<div style={{ position: 'relative', width: '100%', height: '300px' }}>
{activeSrc ? (
activeSrc.startsWith('data:') ? (
<img
src={activeSrc}
alt={room.name}
style={{ width: '100%', height: '300px', objectFit: 'cover' }}
/>
) : (
<OptimizedImage
id={parseInt(activeSrc.split('id=')[1])}
alt={room.name}
size="medium"
lazy={true}
style={{ width: '100%', height: '300px' }}
/>
)
) : (
<div style={{ width: '100%', height: '300px', background: '#ddd', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
No photo
</div>
)}
{/* Like button with count */}
<button
onClick={() => toggleLike(room.id)}
style={{
position: 'absolute',
top: 12,
left: 12,
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '6px 12px',
borderRadius: '999px',
border: 'none',
background: hasLiked ? 'rgba(231,76,60,0.9)' : 'rgba(0,0,0,0.5)',
color: '#fff',
fontSize: '14px',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }}
aria-label="Like this room"
>
<span style={{ fontSize: '16px' }}>{hasLiked ? '❤️' : '♡'}</span>
<span>{totalLikes}</span>
</button>
</div>
{/* ─── Thumbnail Strip ─── */}
{photos.length > 1 && (
<div style={{
display: 'flex',
gap: '6px',
padding: '10px 14px 14px',
overflowX: 'auto',
}}>
{photos.map((p, i) => {
const isActive = i === activeIdx;
return (
<button
key={`${room.id}-${i}`}
onClick={() => setActivePhotoByRoom((prev) => ({ ...prev, [room.id]: i }))}
style={{
width: 60,
height: 45,
flexShrink: 0,
borderRadius: 8,
border: isActive ? `2px solid ${gold}` : '2px solid transparent',
padding: 0,
cursor: 'pointer',
overflow: 'hidden',
transition: 'border-color 0.2s, transform 0.15s',
}}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.transform = 'scale(1.08)';
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.transform = 'scale(1)';
}}
>
{p ? (
p.startsWith('data:') ? (
<img
src={p}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<OptimizedImage
id={parseInt(p.split('id=')[1])}
alt=""
size="thumbnail"
lazy={true}
style={{ width: '100%', height: '100%' }}
/>
)
) : (
<div style={{ width: '100%', height: '100%', background: '#ddd' }} />
)}
</button>
);
})}
</div>
)}
{/* ─── Room Info ─── */}
<div style={{ padding: '1.5rem', flex: 1, display: 'flex', flexDirection: 'column' }}>
<h2 style={{ margin: 0, color: navy, fontSize: '24px' }}>{room.name}</h2>
<p style={{ color: '#555', lineHeight: 1.5, flex: 1, marginTop: '0.5rem' }}>{room.description}</p>
{/* ─── Amenity Icons ─── */}
{room.amenities && room.amenities.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem', marginTop: '0.75rem' }}>
{room.amenities.slice(0, 6).map((amenity) => (
<div
key={amenity.id}
title={amenity.name}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.35rem',
padding: '0.35rem 0.6rem',
background: 'rgba(1,13,30,0.06)',
borderRadius: '999px',
fontSize: '12px',
color: navy,
whiteSpace: 'nowrap',
}}
>
{amenity.icon_svg ? (
<span
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px' }}
dangerouslySetInnerHTML={{ __html: amenity.icon_svg
.replace(/width="[^"]*"/g, '')
.replace(/height="[^"]*"/g, '')
.replace(/stroke="currentColor"/g, `stroke="${navy}"`)
.replace(/<svg/, '<svg style="width:100%;height:100%"')
}}
/>
) : null}
<span>{amenity.name}</span>
</div>
))}
{room.amenities.length > 6 && (
<span style={{ fontSize: '10px', color: '#777', alignSelf: 'center', padding: '0 0.5rem' }}>+{room.amenities.length - 6}</span>
)}
</div>
)}
<div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: warm, fontWeight: 700, fontSize: '18px' }}>${room.price}</span>
<span style={{ fontSize: '13px', color: '#777' }}>per night</span>
</div>
{/* ─── Action Buttons ─── */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem', marginTop: '1rem' }}>
<button
onClick={() => openModal('message', room)}
style={{
padding: '0.6rem 0.5rem',
borderRadius: '10px',
border: `2px solid ${navy}`,
background: 'transparent',
color: navy,
fontWeight: 600,
fontSize: '12px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
Message
</button>
<button
onClick={() => openModal('calendar', room)}
style={{
padding: '0.6rem 0.5rem',
borderRadius: '10px',
border: `2px solid ${navy}`,
background: 'transparent',
color: navy,
fontWeight: 600,
fontSize: '12px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
Reserve
</button>
<button
onClick={() => openModal('review', room)}
style={{
padding: '0.6rem 0.5rem',
borderRadius: '10px',
border: `2px solid ${navy}`,
background: 'transparent',
color: navy,
fontWeight: 600,
fontSize: '12px',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
Review
</button>
</div>
{/* Comments button — cream */}
<button
onClick={() => setExpandedRoom(isExpanded ? null : room.id)}
style={{
marginTop: '1rem',
padding: '0.6rem 1rem',
borderRadius: '999px',
border: `2px solid ${cream}`,
background: cream,
color: navy,
cursor: 'pointer',
fontWeight: 600,
fontSize: '14px',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = gold; e.currentTarget.style.borderColor = gold; e.currentTarget.style.color = navy; }}
onMouseLeave={(e) => { e.currentTarget.style.background = cream; e.currentTarget.style.borderColor = cream; e.currentTarget.style.color = navy; }}
>
{isExpanded ? 'Hide comments' : `Comments (${comments.length})`}
</button>
{/* Comments section */}
{isExpanded && (
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{loadingComments[room.id] && <p style={{ color: '#777', fontSize: '13px' }}>Loading comments...</p>}
{user ? (
user.comments_disabled ? (
<p style={{ color: '#c23b22', fontSize: '14px' }}>Comments are disabled for your account.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder="Share your thoughts about this room..."
rows={3}
style={{
padding: '0.75rem',
borderRadius: '10px',
border: '1px solid rgba(1,13,30,0.18)',
fontFamily: 'inherit',
fontSize: '14px',
resize: 'vertical',
}}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '12px', color: '#777' }}>Posting as {user.first_name || user.username}</span>
<button
onClick={() => postComment(room.id)}
disabled={sending || !draft.trim()}
style={{
padding: '0.5rem 1rem',
borderRadius: '8px',
border: 'none',
background: gold,
color: navy,
fontWeight: 700,
cursor: draft.trim() ? 'pointer' : 'default',
opacity: draft.trim() ? 1 : 0.5,
}}
>
{sending ? 'Sending...' : 'Post'}
</button>
</div>
</div>
)
) : (
<p style={{ color: '#777', fontSize: '14px' }}>Log in to leave a comment.</p>
)}
{comments.length === 0 ? (
<p style={{ color: '#777', fontSize: '14px' }}>No comments yet.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{comments.map((c) => (
<div key={c.id} style={{ background: '#fff', padding: '0.75rem', borderRadius: '10px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.3rem' }}>
<strong style={{ color: navy, fontSize: '14px' }}>
{c.first_name}{c.last_initial ? ` ${c.last_initial}.` : ''}
</strong>
<span style={{ fontSize: '12px', color: '#777' }}>
{formatDisplayDate(c.created_at)}
</span>
</div>
<p style={{ margin: 0, color: '#555', fontSize: '14px' }}>{c.text}</p>
{user?.role === 'admin' && (
<button
onClick={() => deleteComment(room.id, c.id)}
style={{ marginTop: '0.5rem', fontSize: '12px', color: '#c23b22', background: 'none', border: 'none', cursor: 'pointer' }}
>
Delete
</button>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
</article>
);
})}
</div>
{/* ─── Modals ─── */}
{activeModal && selectedRoom && (
<div
onClick={closeModal}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '1rem',
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: '#fff',
borderRadius: '20px',
padding: '2rem',
maxWidth: '500px',
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
}}
>
{/* Reserve Modal */}
{activeModal === 'reserve' && (
<>
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Reserve {selectedRoom.name}</h3>
{!user ? (
<p style={{ color: '#777' }}>Please <a href="/login" style={{ color: gold }}>log in</a> to make a reservation.</p>
) : (
<>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-in Date</label>
<input
type="date"
value={reservationDates.checkIn}
onChange={(e) => setReservationDates({ ...reservationDates, checkIn: e.target.value })}
min={new Date().toISOString().split('T')[0]}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-out Date</label>
<input
type="date"
value={reservationDates.checkOut}
onChange={(e) => setReservationDates({ ...reservationDates, checkOut: e.target.value })}
min={reservationDates.checkIn || new Date().toISOString().split('T')[0]}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
/>
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
<button onClick={handleReserve} disabled={submitting || !reservationDates.checkIn || !reservationDates.checkOut} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Processing...' : 'Request Reservation'}</button>
</div>
</>
)}
</>
)}
{/* Message Modal */}
{activeModal === 'message' && (
<>
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Send a Message</h3>
<p style={{ color: '#555', marginBottom: '1rem' }}>Send a message about {selectedRoom.name} to our team.</p>
{!user && (
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#f8f8f8', borderRadius: '10px' }}>
<div style={{ marginBottom: '0.75rem' }}>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Your Name *</label>
<input
type="text"
value={guestForm.name}
onChange={(e) => setGuestForm({ ...guestForm, name: e.target.value })}
placeholder="Enter your name"
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
/>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Preferred Contact Method</label>
<select
value={guestForm.contactMethod}
onChange={(e) => setGuestForm({ ...guestForm, contactMethod: e.target.value })}
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
>
<option value="email">Email</option>
<option value="whatsapp">WhatsApp</option>
<option value="phone">Phone</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>
{guestForm.contactMethod === 'email' ? 'Email Address' : guestForm.contactMethod === 'whatsapp' ? 'WhatsApp Number' : 'Phone Number'} *
</label>
<input
type={guestForm.contactMethod === 'email' ? 'email' : 'tel'}
value={guestForm.contact}
onChange={(e) => setGuestForm({ ...guestForm, contact: e.target.value })}
placeholder={guestForm.contactMethod === 'email' ? 'your@email.com' : '+593 99 999 9999'}
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
/>
</div>
</div>
)}
{user && (
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8f8f8', borderRadius: '10px', fontSize: '13px', color: '#555' }}>
<strong>From:</strong> {user.first_name} {user.last_name} ({user.email})
</div>
)}
<textarea
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
placeholder="Your message..."
rows={5}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px', resize: 'vertical' }}
/>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
<button onClick={handleSendMessage} disabled={submitting || !messageText.trim() || (!user && (!guestForm.name.trim() || !guestForm.contact.trim()))} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Sending...' : 'Send Message'}</button>
</div>
</>
)}
{/* Reserve/Calendar Modal */}
{activeModal === 'calendar' && (
<>
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Reserve {selectedRoom.name}</h3>
<p style={{ color: '#555', marginBottom: '1rem' }}>Select your dates and we'll contact you to confirm.</p>
{!user && (
<div style={{ marginBottom: '1rem', padding: '1rem', background: '#f8f8f8', borderRadius: '10px' }}>
<div style={{ marginBottom: '0.75rem' }}>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Your Name *</label>
<input
type="text"
value={guestForm.name}
onChange={(e) => setGuestForm({ ...guestForm, name: e.target.value })}
placeholder="Enter your name"
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
/>
</div>
<div style={{ marginBottom: '0.75rem' }}>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>Preferred Contact Method</label>
<select
value={guestForm.contactMethod}
onChange={(e) => setGuestForm({ ...guestForm, contactMethod: e.target.value })}
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
>
<option value="email">Email</option>
<option value="whatsapp">WhatsApp</option>
<option value="phone">Phone</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.25rem', fontWeight: 600, color: navy, fontSize: '13px' }}>
{guestForm.contactMethod === 'email' ? 'Email Address' : guestForm.contactMethod === 'whatsapp' ? 'WhatsApp Number' : 'Phone Number'} *
</label>
<input
type={guestForm.contactMethod === 'email' ? 'email' : 'tel'}
value={guestForm.contact}
onChange={(e) => setGuestForm({ ...guestForm, contact: e.target.value })}
placeholder={guestForm.contactMethod === 'email' ? 'your@email.com' : '+593 99 999 9999'}
style={{ width: '100%', padding: '0.5rem', borderRadius: '8px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px' }}
/>
</div>
</div>
)}
{user && (
<div style={{ marginBottom: '1rem', padding: '0.75rem', background: '#f8f8f8', borderRadius: '10px', fontSize: '13px', color: '#555' }}>
<strong>Reserving as:</strong> {user.first_name} {user.last_name} ({user.email})
</div>
)}
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-in Date *</label>
<input
type="date"
value={reservationDates.checkIn}
onChange={(e) => setReservationDates({ ...reservationDates, checkIn: e.target.value })}
min={new Date().toISOString().split('T')[0]}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Check-out Date *</label>
<input
type="date"
value={reservationDates.checkOut}
onChange={(e) => setReservationDates({ ...reservationDates, checkOut: e.target.value })}
min={reservationDates.checkIn || new Date().toISOString().split('T')[0]}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '16px' }}
/>
</div>
<div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 600, color: navy }}>Availability Calendar</label>
<RoomCalendar roomId={selectedRoom.id} />
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
<button onClick={handleReserve} disabled={submitting || !reservationDates.checkIn || !reservationDates.checkOut || (!user && (!guestForm.name.trim() || !guestForm.contact.trim()))} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Processing...' : 'Request Reservation'}</button>
</div>
</>
)}
{/* Review Modal */}
{activeModal === 'review' && (
<>
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Leave a Review</h3>
<p style={{ color: '#555', marginBottom: '1rem' }}>Share your experience with {selectedRoom.name}.</p>
{!user ? (
<p style={{ color: '#777' }}>Please <a href="/login" style={{ color: gold }}>log in</a> to leave a review.</p>
) : (
<>
<textarea
value={reviewText}
onChange={(e) => setReviewText(e.target.value)}
placeholder="Your review or suggestion..."
rows={5}
style={{ width: '100%', padding: '0.75rem', borderRadius: '10px', border: '1px solid rgba(1,13,30,0.2)', fontSize: '14px', resize: 'vertical' }}
/>
<p style={{ fontSize: '12px', color: '#777', marginTop: '0.5rem' }}>Reviews are submitted for admin approval before being published.</p>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
<button onClick={closeModal} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: `2px solid ${navy}`, background: 'transparent', color: navy, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
<button onClick={handleReview} disabled={submitting || !reviewText.trim()} style={{ flex: 1, padding: '0.75rem', borderRadius: '10px', border: 'none', background: gold, color: navy, fontWeight: 600, cursor: 'pointer', opacity: submitting ? 0.6 : 1 }}>{submitting ? 'Submitting...' : 'Submit Review'}</button>
</div>
</>
)}
</>
)}
</div>
</div>
)}
</main>
);
}