Untitled

 avatar
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