Untitled
unknown
plain_text
8 months ago
14 kB
9
Indexable
import React, { useState, useEffect } from "react";
// Single-file React app (Tailwind-ready) for a single hotel with multiple rooms.
// Features:
// - Room listing with capacities & prices
// - Simple availability check by date range
// - Booking form (stores bookings in localStorage)
// - Lightweight account system (signup/login stored in localStorage)
// - Admin view to see/manage bookings
// - Payment placeholder (integrate Stripe/Paystack/MoMo later)
export default function HotelReservationApp() {
const sampleRooms = [
{ id: "r1", name: "Standard Double", beds: 1, capacity: 2, price: 40 },
{ id: "r2", name: "Deluxe Double", beds: 2, capacity: 3, price: 60 },
{ id: "r3", name: "Family Suite", beds: 3, capacity: 5, price: 100 },
];
// App state
const [rooms] = useState(sampleRooms);
const [bookings, setBookings] = useState([]);
const [user, setUser] = useState(null); // {email, name}
const [view, setView] = useState("home"); // home, account, admin
const [selectedRoom, setSelectedRoom] = useState(null);
const [message, setMessage] = useState(null);
// Load bookings & user from localStorage
useEffect(() => {
const b = JSON.parse(localStorage.getItem("bookings") || "[]");
setBookings(b);
const u = JSON.parse(localStorage.getItem("hr_user") || "null");
setUser(u);
}, []);
useEffect(() => {
localStorage.setItem("bookings", JSON.stringify(bookings));
}, [bookings]);
// Helpers
function datesOverlap(startA, endA, startB, endB) {
return !(endA < startB || startA > endB);
}
function isRoomAvailable(roomId, startDate, endDate) {
const s = new Date(startDate).setHours(0, 0, 0, 0);
const e = new Date(endDate).setHours(0, 0, 0, 0);
return !bookings.some((b) => b.roomId === roomId && datesOverlap(s, e, new Date(b.start).setHours(0,0,0,0), new Date(b.end).setHours(0,0,0,0)));
}
// Booking flow
function openBooking(room) {
setSelectedRoom(room);
setView("book");
setMessage(null);
}
function handleCreateBooking({ name, email, roomId, start, end, guests }) {
const startT = new Date(start).setHours(0,0,0,0);
const endT = new Date(end).setHours(0,0,0,0);
if (endT < startT) {
setMessage({ type: "error", text: "Checkout date must be after check-in." });
return;
}
if (!isRoomAvailable(roomId, startT, endT)) {
setMessage({ type: "error", text: "Room is not available for that period." });
return;
}
const room = rooms.find(r => r.id === roomId);
const nights = Math.round((endT - startT) / (1000*60*60*24)) || 1;
const total = nights * room.price;
const booking = {
id: "b_" + Math.random().toString(36).slice(2,9),
name,
email,
roomId,
roomName: room.name,
start: new Date(startT).toISOString(),
end: new Date(endT).toISOString(),
guests,
nights,
total,
status: "pending",
createdAt: new Date().toISOString()
};
setBookings(prev => [booking, ...prev]);
setMessage({ type: "success", text: `Booking created — total $${total}. Use the account page to view.` });
setView("account");
// if user exists and email matches, keep logged
const storedUser = JSON.parse(localStorage.getItem("hr_user") || "null");
if (!storedUser || storedUser.email !== email) {
// keep identity simple: store email+name as last booker
localStorage.setItem("hr_user", JSON.stringify({ name, email }));
setUser({ name, email });
}
}
// Account management
function signup(name, email) {
localStorage.setItem("hr_user", JSON.stringify({ name, email }));
setUser({ name, email });
setMessage({ type: "success", text: "Account created and logged in." });
setView("home");
}
function logout() {
localStorage.removeItem("hr_user");
setUser(null);
setMessage({ type: "info", text: "Logged out." });
setView("home");
}
// Admin quick actions (simple auth: [email protected])
const isAdmin = user && user.email === "[email protected]";
function adminConfirm(bookingId) {
setBookings(prev => prev.map(b => b.id === bookingId ? { ...b, status: "confirmed" } : b));
}
function adminCancel(bookingId) {
setBookings(prev => prev.map(b => b.id === bookingId ? { ...b, status: "cancelled" } : b));
}
// UI Components
function Header() {
return (
<header className="w-full bg-gradient-to-r from-slate-800 to-slate-700 text-white p-4 flex items-center justify-between">
<h1 className="text-xl font-bold">MyHotel — Reservation</h1>
<nav className="flex gap-3 items-center">
<button className="text-sm" onClick={() => { setView('home'); setMessage(null); }}>Home</button>
<button className="text-sm" onClick={() => { setView('account'); setMessage(null); }}>My Account</button>
{isAdmin && <button className="text-sm" onClick={() => setView('admin')}>Admin</button>}
{user ? (
<div className="text-sm ml-4">Hello, <strong>{user.name}</strong> <button className="ml-2 underline" onClick={logout}>Logout</button></div>
) : (
<div className="text-sm ml-4">Not logged</div>
)}
</nav>
</header>
);
}
function HomeView() {
return (
<div className="p-6">
<h2 className="text-2xl font-semibold mb-4">Rooms</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{rooms.map(r => (
<div key={r.id} className="p-4 border rounded-lg shadow-sm">
<h3 className="text-lg font-bold">{r.name}</h3>
<p>Capacity: {r.capacity} | Beds: {r.beds}</p>
<p className="mt-2">Price / night: ${r.price}</p>
<div className="mt-4 flex gap-2">
<button className="px-3 py-1 bg-slate-800 text-white rounded" onClick={() => openBooking(r)}>Book</button>
<button className="px-3 py-1 border rounded" onClick={() => { setView('details'); setSelectedRoom(r); }}>Details</button>
</div>
</div>
))}
</div>
</div>
);
}
function DetailsView() {
const r = selectedRoom;
if (!r) return <div className="p-6">No room selected.</div>;
// show next 6 months availability summary
return (
<div className="p-6">
<button className="mb-4 underline" onClick={() => setView('home')}>← Back</button>
<h2 className="text-2xl font-semibold">{r.name}</h2>
<p className="mt-2">Capacity: {r.capacity} | Beds: {r.beds}</p>
<p className="mt-2">Price / night: ${r.price}</p>
<div className="mt-4">
<h3 className="font-semibold">Quick availability check</h3>
<small className="block mb-2">Select dates in the booking form to check actual availability.</small>
</div>
</div>
);
}
function BookView() {
const room = selectedRoom || rooms[0];
const [name, setName] = useState(user?.name || "");
const [email, setEmail] = useState(user?.email || "");
const [start, setStart] = useState( new Date().toISOString().slice(0,10) );
const [end, setEnd] = useState( new Date(Date.now() + 24*60*60*1000).toISOString().slice(0,10) );
const [guests, setGuests] = useState(1);
return (
<div className="p-6 max-w-lg">
<button className="mb-4 underline" onClick={() => setView('home')}>← Back</button>
<h2 className="text-2xl font-semibold">Book — {room.name}</h2>
<form onSubmit={(e) => { e.preventDefault(); handleCreateBooking({ name, email, roomId: room.id, start, end, guests }); }} className="mt-4 space-y-3">
<label className="block">
<div className="text-sm">Full name</div>
<input required value={name} onChange={e=>setName(e.target.value)} className="w-full p-2 border rounded" />
</label>
<label className="block">
<div className="text-sm">Email</div>
<input required type="email" value={email} onChange={e=>setEmail(e.target.value)} className="w-full p-2 border rounded" />
</label>
<div className="grid grid-cols-2 gap-2">
<label>
<div className="text-sm">Check-in</div>
<input required type="date" value={start} onChange={e=>setStart(e.target.value)} className="w-full p-2 border rounded" />
</label>
<label>
<div className="text-sm">Check-out</div>
<input required type="date" value={end} onChange={e=>setEnd(e.target.value)} className="w-full p-2 border rounded" />
</label>
</div>
<label>
<div className="text-sm">Guests</div>
<input required type="number" min={1} max={room.capacity} value={guests} onChange={e=>setGuests(Number(e.target.value))} className="w-24 p-2 border rounded" />
</label>
<div className="flex items-center gap-3 mt-2">
<button className="px-4 py-2 bg-green-600 text-white rounded">Confirm booking</button>
<button type="button" className="px-4 py-2 border rounded" onClick={() => { if (isRoomAvailable(room.id, start, end)) { alert('Room is available!'); } else { alert('Not available'); } }}>Check availability</button>
</div>
<div className="pt-3 text-sm">
<strong>Payment:</strong> This demo does not process payments. Integrate Stripe/Paystack/MoMo where marked.
</div>
</form>
{message && <div className={`mt-4 p-3 rounded ${message.type==='error' ? 'bg-red-100 text-red-800' : message.type==='success' ? 'bg-green-100 text-green-800' : 'bg-slate-100'}`}>{message.text}</div>}
</div>
);
}
function AccountView() {
if (!user) {
return <AuthView setMessage={setMessage} signup={signup} setView={setView} />;
}
const myBookings = bookings.filter(b => b.email === user.email);
return (
<div className="p-6">
<h2 className="text-2xl font-semibold">My Account</h2>
<p className="mt-2">Logged in as <strong>{user.name}</strong> — {user.email}</p>
<div className="mt-4">
<h3 className="font-semibold">Your bookings</h3>
{myBookings.length === 0 ? (
<div className="mt-2 text-sm">You have no bookings yet.</div>
) : (
<div className="mt-2 space-y-3">
{myBookings.map(b => (
<div key={b.id} className="p-3 border rounded">
<div className="flex justify-between items-center">
<div>
<div className="font-semibold">{b.roomName}</div>
<div className="text-sm">{new Date(b.start).toLocaleDateString()} → {new Date(b.end).toLocaleDateString()} ({b.nights} nights)</div>
<div className="text-sm">Total: ${b.total} • Status: <strong>{b.status}</strong></div>
</div>
<div className="text-sm">
<button className="px-2 py-1 border rounded" onClick={() => navigator.clipboard?.writeText(b.id)}>Copy ID</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
function AuthView({ signup, setMessage, setView }) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
return (
<div className="p-6 max-w-md">
<h2 className="text-2xl font-semibold">Create account / Login</h2>
<p className="text-sm mt-2">This demo uses a simple email-based account saved to your browser.</p>
<form onSubmit={(e)=>{ e.preventDefault(); signup(name, email); }} className="mt-4 space-y-3">
<label>
<div className="text-sm">Full name</div>
<input required value={name} onChange={e=>setName(e.target.value)} className="w-full p-2 border rounded" />
</label>
<label>
<div className="text-sm">Email</div>
<input required type="email" value={email} onChange={e=>setEmail(e.target.value)} className="w-full p-2 border rounded" />
</label>
<div className="flex gap-2">
<button className="px-4 py-2 bg-slate-800 text-white rounded">Create / Login</button>
<button type="button" className="px-4 py-2 border rounded" onClick={()=>{ setMessage({ type: 'info', text: 'Try admin by creating user [email protected]' }); }}>Need admin?</button>
</div>
</form>
</div>
);
}
function AdminView() {
return (
<div className="p-6">
<h2 className="text-2xl font-semibold">Admin — Bookings</h2>
<div className="mt-4 space-y-3">
{bookings.length === 0 ? <div>No bookings</div> : bookings.map(b => (
<div key={b.id} className="p-3 border rounded flex justify-between items-center">
<div>
<div className="font-semibold">{b.roomName} — {b.name}</div>
<div className="text-sm">{new Date(b.start).toLocaleDateString()} → {new Date(b.end).toLocaleDateString()} ({b.nights} nights) • ${b.total}</div>
<div className="text-sm">Email: {b.email} • Status: <strong>{b.status}</strong></div>
</div>
<div className="flex gap-2">
<button className="px-2 py-1 border rounded" onClick={()=>adminConfirm(b.id)}>Confirm</button>
<button className="px-2 py-1 border rounded" onClick={()=>adminCancel(b.id)}>Cancel</button>
</div>
</div>
))}
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 text-gray-800">
<Header />
<main className="max-w-6xl mx-auto">
{message && <div className="p-3 bg-yellow-50 text-yellow-800 text-sm">{message.text}</div>}
{view === 'home' && <HomeView />}
{view === 'details' && <DetailsView />}
{view === 'book' && <BookView />}
{view === 'account' && <AccountView />}
{view === 'admin' && isAdmin && <AdminView />}
{!isAdmin && view === 'admin' && <div className="p-6">Admin access restricted. Log in as <strong>[email protected]</strong>.</div>}
<footer className="p-6 text-sm text-center text-gray-500">Demo — integrate real backend, payments, and translations before production.</footer>
</main>
</div>
);
}
Editor is loading...
Leave a Comment