source: frontend/src/pages/SearchRoomsPage.jsx@ 09e02d7

Last change on this file since 09e02d7 was 09e02d7, checked in by Nikola Sarafimov <sarafimov.nikola12345@…>, 4 days ago

Final room reservation system implementation

  • Property mode set to 100644
File size: 7.3 KB
RevLine 
[09e02d7]1import { useState } from "react";
2import { motion } from "framer-motion";
3import toast from "react-hot-toast";
4import { DoorOpen, Search, SlidersHorizontal } from "lucide-react";
5import { searchRooms } from "../api.js";
6
7export default function SearchRoomsPage() {
8 const [form, setForm] = useState({
9 reservationDate: "2026-07-20",
10 startTime: "09:00",
11 endTime: "10:00",
12 minimumCapacity: 1,
13 roomType: "",
14 requiredEquipmentName: "",
15 });
16
17 const [rooms, setRooms] = useState([]);
18 const [searched, setSearched] = useState(false);
19 const [loading, setLoading] = useState(false);
20
21 function updateField(field, value) {
22 setForm((prev) => ({ ...prev, [field]: value }));
23 }
24
25 async function submit(e) {
26 e.preventDefault();
27
28 if (!form.reservationDate || !form.startTime || !form.endTime) {
29 toast.error("Reservation date, start time and end time are required.");
30 return;
31 }
32
33 if (form.endTime <= form.startTime) {
34 toast.error("End time must be after start time.");
35 return;
36 }
37
38 if (Number(form.minimumCapacity) < 1) {
39 toast.error("Minimum capacity must be greater than 0.");
40 return;
41 }
42
43 setLoading(true);
44 setSearched(true);
45
46 try {
47 const result = await searchRooms({
48 ...form,
49 minimumCapacity: Number(form.minimumCapacity),
50 });
51
52 setRooms(result);
53 toast.success(`${result.length} available rooms found`);
54 } catch (err) {
55 setRooms([]);
56 toast.error(err.message);
57 } finally {
58 setLoading(false);
59 }
60 }
61
62 return (
63 <section>
64 <Hero
65 eyebrow="Availability search"
66 title="Search Available Rooms"
67 subtitle="Find rooms that satisfy capacity, type, equipment and time availability constraints."
68 />
69
70 <div className="workspace-grid">
71 <form className="panel page-form" onSubmit={submit}>
72 <SectionTitle
73 icon={SlidersHorizontal}
74 title="Search filters"
75 subtitle="Define the reservation interval and optional room requirements."
76 />
77
78 <div className="form-grid">
79 <Input
80 label="Reservation date"
81 type="date"
82 value={form.reservationDate}
83 onChange={(value) => updateField("reservationDate", value)}
84 />
85
86 <Input
87 label="Start time"
88 type="time"
89 value={form.startTime}
90 onChange={(value) => updateField("startTime", value)}
91 />
92
93 <Input
94 label="End time"
95 type="time"
96 value={form.endTime}
97 onChange={(value) => updateField("endTime", value)}
98 />
99
100 <Input
101 label="Minimum capacity"
102 type="number"
103 value={form.minimumCapacity}
104 onChange={(value) => updateField("minimumCapacity", value)}
105 />
106
107 <label>
108 Room type
109 <select value={form.roomType} onChange={(e) => updateField("roomType", e.target.value)}>
110 <option value="">Any type</option>
111 <option value="classroom">classroom</option>
112 <option value="office">office</option>
113 <option value="meeting_room">meeting_room</option>
114 <option value="lab">lab</option>
115 </select>
116 </label>
117
118 <Input
119 label="Required equipment"
120 value={form.requiredEquipmentName}
121 placeholder="Example: Projector"
122 onChange={(value) => updateField("requiredEquipmentName", value)}
123 />
124 </div>
125
126 <button className="primary full" disabled={loading}>
127 {loading ? "Searching..." : "Search rooms"}
128 <Search size={18} />
129 </button>
130 </form>
131
132 <div className="panel">
133 <SectionTitle
134 icon={DoorOpen}
135 title="Available rooms"
136 subtitle="Results are loaded from the live faculty PostgreSQL database."
137 />
138
139 {loading && <div className="empty-state">Searching database...</div>}
140
141 {!loading && searched && rooms.length === 0 && (
142 <div className="empty-state">No available rooms found for the selected criteria.</div>
143 )}
144
145 {!loading && rooms.length > 0 && (
146 <div className="room-grid">
147 {rooms.map((room) => (
148 <RoomCard key={room.roomId} room={room} />
149 ))}
150 </div>
151 )}
152
153 {!searched && !loading && (
154 <div className="empty-state">Run a search to display available rooms.</div>
155 )}
156 </div>
157 </div>
158 </section>
159 );
160}
161
162function RoomCard({ room }) {
163 return (
164 <motion.div className="room-card" whileHover={{ y: -5 }}>
165 <div className="room-card-top">
166 <DoorOpen size={24} />
167 <Badge value={room.type} />
168 </div>
169
170 <h3>{room.roomCode}</h3>
171 <p>{room.buildingName}</p>
172
173 <div className="room-meta">
174 <span>Capacity</span>
175 <strong>{room.capacity}</strong>
176 </div>
177
178 <small>{room.address}</small>
179 </motion.div>
180 );
181}
182
183function Hero({ eyebrow, title, subtitle }) {
184 return (
185 <div className="hero">
186 <div>
187 <div className="eyebrow">
188 <Search size={16} />
189 {eyebrow}
190 </div>
191 <h1>{title}</h1>
192 <p>{subtitle}</p>
193 </div>
194 </div>
195 );
196}
197
198function SectionTitle({ icon: Icon, title, subtitle }) {
199 return (
200 <div className="section-title">
201 <Icon size={21} />
202 <div>
203 <h3>{title}</h3>
204 <p>{subtitle}</p>
205 </div>
206 </div>
207 );
208}
209
210function Input({ label, type = "text", value, placeholder = "", onChange }) {
211 return (
212 <label>
213 {label}
214 <input type={type} value={value} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />
215 </label>
216 );
217}
218
219function Badge({ value }) {
220 return <span className={`badge ${String(value || "").toLowerCase()}`}>{value}</span>;
221}
Note: See TracBrowser for help on using the repository browser.