source: my-react-app/src/App.js@ c44c5ed

main
Last change on this file since c44c5ed was c44c5ed, checked in by Aleksandar Panovski <apano77@…>, 2 weeks ago

Fixed bugs and fully developed menu now. Readonly added.

  • Property mode set to 100644
File size: 16.4 KB
RevLine 
[8ca35dc]1import {BrowserRouter as Router, Navigate, Route, Routes, useNavigate} from 'react-router-dom';
[f5b256e]2
[d24f17c]3import Customers from './components/Customers';
4import Layout from "./components/Layout";
[cfc16a3]5import React, {useContext, useEffect, useState} from 'react';
[d24f17c]6import CustomerFormContainer from "./components/CustomerFormContainer";
7import CustomerDetails from "./components/CustomerDetails";
8import ErrorPage from "./components/ErrorPage";
9import Restaurants from "./components/Restaurants";
10import Reservations from "./components/Reservations";
11import RestaurantDetails from "./components/RestaurantDetails";
12import ReservationConfirmation from "./components/ReservationConfirmation";
13import ReservationEdit from "./components/ReservationEdit";
[65b6638]14import axios from "axios";
[cfc16a3]15import { CuisineContext } from './components/CuisineContext';
[9293100]16import RestaurantInfo from "./components/RestaurantInfo";
[8ca35dc]17import AuthForm from "./components/AuthForm";
[24819a8]18import AppContent from "./components/AppContent";
[8ca35dc]19import ReservationHistory from "./components/ReservationHistory";
[f5b256e]20import AuthContent from "./components/AuthContent";
[c44c5ed]21import MenuList from "./components/MenuList";
22import ReadOnlyMenuList from "./components/ReadOnlyMenuList";
[8ca35dc]23
24const ProtectedRoute = ({ element, isAuthenticated }) => {
25 return isAuthenticated ? element : <Navigate to="/login" />;
26};
[d24f17c]27
28const App = () => {
[f5b256e]29 const [isAuthenticated, setIsAuthenticated] = useState(false);
[8ca35dc]30
[f5b256e]31 useEffect(() => {
[8ca35dc]32 const token = localStorage.getItem('token');
33 if (token) {
34 setIsAuthenticated(true);
35 }
36 }, []);
37
[d24f17c]38 return (
39 <Router>
40 <Layout>
41 <Routes>
42 <Route path="/" element={<Home />} />
[8ca35dc]43 <Route path="/customers" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<Customers />} />} />
44 <Route path="/customers/add" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<CustomerFormContainer />} />} />
45 <Route path="/customers/:id" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<CustomerDetails />} />} />
46 <Route path="/customers/edit/:id" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<CustomerFormContainer />} />} />
47 <Route path="/restaurants" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<Restaurants />} />} />
48 <Route path="/restaurants/:id" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<RestaurantDetails />} />} />
49 <Route path="/reservations" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<Reservations />} />} />
50 <Route path="/reservationConfirmation/:tableNumber/:timeSlot/:restaurantId" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<ReservationConfirmation />} />} />
51 <Route path="/reservations/reservationEdit/:reservationId" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<ReservationEdit />} />} />
52 <Route path="/reservations-past" element={<ProtectedRoute isAuthenticated={isAuthenticated} element={<ReservationHistory />} />} />
53 <Route path="/login" element={<AuthForm setIsAuthenticated={setIsAuthenticated} />} />
54
55 <Route path="/error" element={<ErrorPage />} />
[d24f17c]56 </Routes>
57 </Layout>
58 </Router>
59 );
[8ca35dc]60};
[d24f17c]61
62
63const Home = () => {
[e35d0e9]64 const navigate = useNavigate();
65
[f5b256e]66 const todayDate = new Date().toISOString().split('T')[0];
[d24f17c]67
68 const [date, setDate] = useState(todayDate);
69 const [selectedTime, setSelectedTime] = useState('');
70 const [numPeople, setNumPeople] = useState(2);
71 const [searchValue, setSearchValue] = useState('');
72 const [timeSlots, setTimeSlots] = useState([]);
[9293100]73 let [filteredRestaurants, setFilteredRestaurants] = useState([]);
[e35d0e9]74
[cfc16a3]75 const cuisineTypes = useContext(CuisineContext);
[9293100]76 const [showCuisineSearch, setShowCuisineSearch] = useState(true);
[cfc16a3]77
[e35d0e9]78 const [formatedDateTime, setFormatedDateTime] = useState('')
79
[d24f17c]80 useEffect(() => {
81 if (date) {
82 const selectedDate = new Date(date);
83 const today = new Date();
84 const isToday = selectedDate.toDateString() === today.toDateString();
85
[65b6638]86 let startHour = 9;
87 let startMinute = 0;
88 if (isToday) {
89 const currentHour = today.getHours();
90 const currentMinute = today.getMinutes();
91 if (currentHour > 9 || (currentHour === 9 && currentMinute >= 0)) {
92 startHour = currentHour;
93 startMinute = Math.ceil(currentMinute / 15) * 15;
94 }
95 }
[d24f17c]96
[65b6638]97 const startTime = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), startHour, startMinute);
[d24f17c]98 const endTime = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), selectedDate.getDate(), 23, 30);
99
100 const slots = [];
[65b6638]101 let currentTime = new Date(startTime);
[d24f17c]102 while (currentTime <= endTime) {
103 const option = currentTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
104 slots.push(option);
105 currentTime.setMinutes(currentTime.getMinutes() + 15); // Increment by 15 minutes
106 }
107
108 setTimeSlots(slots);
109 }
110 }, [date]);
111
[f5b256e]112 const handleGoToRestaurant = (restaurantId) => {
113 navigate(`/restaurants/${restaurantId}`);
114 };
115
[d24f17c]116 const handleDateChange = (e) => {
117 setDate(e.target.value);
118 };
119
120 const handleTimeChange = (e) => {
121 setSelectedTime(e.target.value);
122 };
123
124 const handleNumPeopleChange = (e) => {
125 setNumPeople(e.target.value);
126 };
127
128 const handleInputChange = (event) => {
129 setSearchValue(event.target.value);
130 };
131
[65b6638]132 const handleSubmit = async (e) => {
[d24f17c]133 e.preventDefault();
[65b6638]134 const [year, month, day] = date.split("-");
135 let formattedDateTime;
[cfc16a3]136
137 if (selectedTime) {
138 const [selectedHours, selectedMinutes] = selectedTime.split(":");
139 if (!isNaN(selectedHours) && !isNaN(selectedMinutes)) {
140 const dateTime = new Date(Date.UTC(year, month - 1, day, selectedHours, selectedMinutes));
141 formattedDateTime = dateTime.toISOString().slice(0, 16).replace('T', ' ');
[9293100]142 setFormatedDateTime(formattedDateTime);
[cfc16a3]143 }
[65b6638]144 } else {
145 const now = new Date();
[f5b256e]146 const currentTime = now.getHours() * 60 + now.getMinutes();
[cfc16a3]147 const nextSlot = timeSlots.find(slot => {
148 const [hours, minutes] = slot.split(":");
[f5b256e]149 const slotTime = parseInt(hours) * 60 + parseInt(minutes);
[cfc16a3]150 return slotTime > currentTime;
151 });
152
153 formattedDateTime = nextSlot ? `${date} ${nextSlot}` : `${date} ${timeSlots[0]}`;
[65b6638]154 }
155
156 const data = {
157 dateTime: formattedDateTime,
158 partySize: numPeople,
159 search: searchValue
160 };
161
162 try {
[8ca35dc]163 const response = await axios.post('http://localhost:8081/api/search', data);
[65b6638]164 const filteredRestaurants = response.data;
[9293100]165 setFilteredRestaurants(filteredRestaurants);
[f5b256e]166 console.log(filteredRestaurants)
[9293100]167 setShowCuisineSearch(false);
[65b6638]168 } catch (error) {
169 console.error('Error:', error);
170 }
[d24f17c]171 };
172
[cfc16a3]173 const handleSearchByCuisine = async (cuisine) => {
174 const cuisineName = cuisine.replace('Searching by cuisine: ', '');
175 try {
[8ca35dc]176 const response = await axios.post(`http://localhost:8081/api/search/shortcut/${cuisineName}`, cuisineName);
[9293100]177 setFilteredRestaurants(response.data)
[f5b256e]178 console.log(response.data)
[cfc16a3]179 } catch (error) {
180 console.error('Error searching by cuisine:', error);
181 }
[9293100]182 setShowCuisineSearch(false);
[cfc16a3]183 };
[f5b256e]184 const parseTime = (timeString) => {
185 const [hours, minutes] = timeString.trim().split(':').map(Number);
186 return new Date().setHours(hours, minutes, 0, 0);
187 };
188 const roundToNextQuarter = (date) => {
189 const minutes = date.getMinutes();
190 const roundedMinutes = Math.floor(minutes / 15) * 15;
191 date.setMinutes(roundedMinutes, 0, 0);
192 return date;
193 };
194
195 const shouldMoveToNextDay = (currentTime, endTime) => {
196 return (endTime - currentTime) <= 2 * 60 * 60 * 1000;
197 };
198 const generateTimeSlots = (operatingHours) => {
199 const timeSlots = [];
200 const [startTimeStr, endTimeStr] = operatingHours.split('-').map((time) => time.trim());
201
202 const startTime = parseTime(startTimeStr);
203 let endTime = parseTime(endTimeStr);
204
205 const currentTime = new Date().getTime();
206 if (shouldMoveToNextDay(currentTime, endTime)) {
207 endTime += 24 * 60 * 60 * 1000;
208 }
209
210 let currentTimeSlot = new Date(startTime);
211 currentTimeSlot = roundToNextQuarter(currentTimeSlot);
212
213 while (currentTimeSlot.getTime() < endTime) {
214 timeSlots.push(currentTimeSlot.toISOString());
215 currentTimeSlot.setMinutes(currentTimeSlot.getMinutes() + 15);
216 }
217
218 return timeSlots;
219 };
220
221 const today = new Date();
222 const year = today.getFullYear();
223 const month = String(today.getMonth() + 1).padStart(2, '0');
224 const day = String(today.getDate()).padStart(2, '0');
225 const formattedDate = `${year}-${month}-${day}`;
[65b6638]226
[e35d0e9]227 const handleTimeSlotClick = (table, timeSlot, restaurant) => {
228 const tableNumber = table.id;
229 const formattedTimeSlot = timeSlot;
230 const restaurantId = restaurant.restaurantId;
231
[f5b256e]232 const encodedTableNumber = encodeURIComponent(tableNumber);
[e35d0e9]233 const encodedTimeSlot = encodeURIComponent(formattedTimeSlot);
234 const encodedRestaurantId = encodeURIComponent(restaurantId);
235
236 navigate(`/reservationConfirmation/${encodedTableNumber}/${encodedTimeSlot}/${encodedRestaurantId}`);
[f5b256e]237 };
[e35d0e9]238
[9293100]239 const renderTimeSlots = (tablesList, restaurant) => {
[f5b256e]240 const currentTime = new Date().getTime();
241 let renderedTimeSlots = {};
[9293100]242
[f5b256e]243 if (tablesList.length === 0) {
244 return <p>No tables available for reservations at this restaurant.</p>;
[9293100]245 }
246
[f5b256e]247 return tablesList.flatMap((table) => {
248 const tableTimeSlots = generateTimeSlots(restaurant.operatingHours);
[9293100]249
[f5b256e]250 if (!renderedTimeSlots[table.capacity]) {
[9293100]251 renderedTimeSlots[table.capacity] = 0;
252 return (
253 <div key={table.capacity}>
[f5b256e]254 <h3>Table for {table.capacity} guests</h3>
255 {tableTimeSlots.map((timeSlot, index) => {
256 const timeSlotTime = new Date(timeSlot).getTime();
257
258 if (timeSlotTime > currentTime && renderedTimeSlots[table.capacity] < 3) {
259 renderedTimeSlots[table.capacity]++;
[9293100]260 const timeSlotDateTime = new Date(timeSlot);
[f5b256e]261 const formattedTime = timeSlotDateTime.toLocaleTimeString([], {
262 hour: '2-digit',
263 minute: '2-digit'
264 });
[9293100]265
266 return (
[f5b256e]267 <button
268 key={index}
269 className="btn btn-primary me-2 mb-2"
270 onClick={() => handleTimeSlotClick(table, timeSlot, restaurant)}
271 >
272 {formattedTime} {}
[9293100]273 </button>
274 );
[f5b256e]275 <br/>
[9293100]276 } else {
[f5b256e]277 return null;
[9293100]278 }
279 })}
280 </div>
281 );
282 } else {
283 return null;
284 }
285 });
[f5b256e]286 };
[65b6638]287
[d24f17c]288
289 return (
290 <div className="container">
[cfc16a3]291 <h2 className="display-1">Rezerviraj masa</h2>
[d24f17c]292 <form className="row g-2 align-items-center" onSubmit={handleSubmit}>
293 <div className="col-auto">
294 <input className="form-control me-2" type="date" value={date} onChange={handleDateChange}
[65b6638]295 min={formattedDate}/>
[d24f17c]296 </div>
297 <div className="col-auto">
298 <select className="form-select" onChange={handleTimeChange}>
299 {timeSlots.map((slot, index) => (
300 <option key={index} value={slot}>{slot}</option>
301 ))}
302 </select>
303 </div>
304 <div className="col-auto">
305 <select className="form-select" value={numPeople} onChange={handleNumPeopleChange}>
306 {[...Array(20).keys()].map((num) => (
307 <option key={num + 1} value={num + 1}>{num + 1}</option>
308 ))}
309 </select>
310 </div>
311 <div className="col-auto">
312 <input
313 className="form-control me-2"
314 type="search"
315 name="search"
316 placeholder="Restaurant or Cuisine"
317 aria-label="Search"
[f5b256e]318 value={searchValue}
319 onChange={handleInputChange}
[d24f17c]320 />
321 </div>
322 <div className="col-auto">
323 <button className="btn btn-outline-success" type="submit">Search</button>
324 </div>
[9293100]325
[e35d0e9]326 <div className="border-0">
[9293100]327 {filteredRestaurants.map((restaurant) => (
328 <div key={restaurant.id} className="card mb-3">
329 <div className="card-body">
[c44c5ed]330 <div className="row">
331 {/* Narrow left column: info and actions */}
332 <div className="col-md-4">
333 <RestaurantInfo key={restaurant.id} restaurant={restaurant}/>
[f5b256e]334 <div className="d-flex flex-wrap">
[c44c5ed]335 {restaurant.tablesList && restaurant.tablesList.length > 0 ? (
336 renderTimeSlots(restaurant.tablesList, restaurant)
337 ) : (
338 <p>No tables available for reservations at this restaurant</p>
339 )}
[f5b256e]340 </div>
[c44c5ed]341 <button
342 className="btn btn-secondary mt-3"
343 onClick={() => handleGoToRestaurant(restaurant.restaurantId)}
344 >
345 Go to Restaurant
346 </button>
347 </div>
348
349 {/* Wide right column: menu */}
350 <div className="col-md-8">
351 <ReadOnlyMenuList restaurantId={restaurant.restaurantId}/>
352 </div>
[9293100]353 </div>
354 </div>
355 </div>
356 ))}
357 </div>
[cfc16a3]358
[c44c5ed]359
[9293100]360 {showCuisineSearch && (
361 <div className="mb-3">
362 <h2 className="display-2">Search by cuisine type</h2>
[f5b256e]363 <ul className="list-group">
364 {cuisineTypes.map((cuisine, index) => (
365 <li key={index} className="list-group-item">
366 <button type="button" className="btn btn-outline-primary"
367 onClick={() => handleSearchByCuisine(cuisine)}>
368 {cuisine}
369 </button>
370 </li>
371 ))}
372 </ul>
373 </div>
374 )}
[d24f17c]375 </form>
376 </div>
377 );
378}
379
380export default App;
Note: See TracBrowser for help on using the repository browser.