I made a server with following REST JSON API:
- Get list of classrooms
/api/v1/room
- Get dates when certain classroom is booked
/api/v1/room/[roomId]
- Get list of teachers
/api/v1/person
- Book(Post) a date in shedule for certain classroom by certain teacher
/api/v1/person/[personId]/room/[roomId]/date/[date]
- Delete(Post) a booking of certain classroom on certain date
/api/v1/room/[roomId]/date/[date]
Data about classrooms, schedule, and teachers taken from JSON files
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const cors = require('cors');
const port = 3001;
function readJsonFile(filename) {
try {
const data = fs.readFileSync(path.join(__dirname, filename), 'utf8');
return JSON.parse(data);
} catch (err) {
console.error(`Ошибка чтения файла ${filename}:`, err);
return { reservation: {"person" : []} };
}
}
const rooms = readJsonFile('room.json');
const persons = readJsonFile('person.json');
let bookings = readJsonFile('bookings.json');
app.use(cors({
origin: `http://localhost:3000`
}))
// GET /api/v1/room
app.get('/api/v1/room', (req, res) => {
res.json(rooms);
});
// GET /api/v1/room/[roomId]
app.get('/api/v1/room/:roomId', (req, res) => {
const roomId = parseInt(req.params.roomId);
const roomBookings = bookings.reservation.filter(booking => booking.roomId === roomId);
res.json(roomBookings.map(booking => booking.date));
});
// GET /api/v1/person
app.get('/api/v1/person', (req, res) => {
res.json(persons);
});
// POST /api/v1/person/[personId]/room/[roomId]/date/[date]
app.post('/api/v1/person/:personId/room/:roomId/date/:date', (req, res) => {
const personId = parseInt(req.params.personId);
const roomId = parseInt(req.params.roomId);
const date = req.params.date;
const person = persons.person.find(p => p.id === personId);
const room = rooms.room.find(r => r.id === roomId);
if (!person || !room) {
return res.status(404).json({ error: 'Человек или комната не найдены' });
}
if (bookings.reservation.find(b => b.roomId === roomId && b.date === date)) {
return res.status(400).json({ error: 'Комната уже забронирована на эту дату' });
}
bookings.reservation.push({ userId: personId, roomId, date });
fs.writeFileSync(path.join(__dirname, 'bookings.json'), JSON.stringify(bookings, null, 2));
res.status(201).json({ message: 'Бронь успешно создана' });
});
// POST /api/v1/room/[roomId]/date/[date]
app.post('/api/v1/room/:roomId/date/:date', (req, res) => {
const roomId = parseInt(req.params.roomId);
const date = req.params.date;
bookings.reservation = bookings.reservation.filter(booking => !(booking.roomId === roomId && booking.date === date));
fs.writeFileSync(path.join(__dirname, 'bookings.json'), JSON.stringify(bookings, null, 2));
res.status(204).send();
});
app.listen(port, () => {
console.log(`Сервер запущен на порту ${port}`);
});
The server is working. Then I tried to make web service for working with that API. The service must provide (a) the possibility of getting a list of all classrooms and bookings for certain classroom, and (b) the possibility of creating/deleting (by certain teacher) a certain booking.
import React, { useState, useEffect } from "react";
import "./App.css";
import Dropdown from "./Dropdown";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import "react-big-calendar/lib/css/react-big-calendar.css";
const localizer = momentLocalizer(moment);
const API_URL = "http://localhost:3001/api/v1";
function App() {
const [isShown, setIsShown] = useState(false);
const [selectedRoomId, setSelectedRoomId] = useState(null);
const [events, setEvents] = useState([]);
const [isDeleting, setIsDeleting] = useState(false);
const [selectedEvent, setSelectedEvent] = useState(null);
const [roomData, setRoomData] = useState([]);
const [bookingData, setBookingData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchRooms().then(() => setIsLoading(false));
}, []);
useEffect(() => {
fetchBookings();
}, [selectedRoomId]);
useEffect(() => {
if (selectedRoomId) {
const filteredBookings = bookingData.filter(
(booking) => booking.roomId === selectedRoomId
);
const calendarEvents = filteredBookings.map((booking) => ({
start: moment(booking.date, "DD.MM.YYYY").toDate(),
end: moment(booking.date, "DD.MM.YYYY").add(1, "day").toDate(),
title: `booked`,
id: booking.userId,
roomId: booking.roomId
}));
setEvents(calendarEvents);
}
}, [selectedRoomId, bookingData]);
const handleClick = (event) => {
setIsShown((current) => !current);
};
const handleRoomChange = (event) => {
setSelectedRoomId(parseInt(event.target.value));
if (roomData.length > 0) {
fetchBookings();
}
};
const handleEventClick = (event) => {
setSelectedEvent(event);
setIsDeleting(true);
};
const handleDeleteConfirm = () => {
deleteBooking(selectedEvent.id, selectedEvent.roomId, selectedEvent.start);
setIsDeleting(false);
};
const handleDeleteCancel = () => {
setIsDeleting(false);
};
const fetchRooms = async () => {
try {
const response = await fetch(`${API_URL}/room`);
const data = await response.json();
setRoomData(data);
} catch (error) {
console.error("Classrom info loading error:", error);
}
};
const fetchBookings = async () => {
try {
const response = await fetch(`${API_URL}/room/${selectedRoomId}`);
const data = await response.json();
setBookingData(data);
} catch (error) {
console.error("Shedule info loading error:", error);
}
};
const deleteBooking = async (userId, roomId, date) => {
try {
const response = await fetch(`${API_URL}/room/${roomId}/date/${date}`, {
method: "POST",
});
if (response.ok) {
fetchBookings();
} else {
console.error("Room booking deleting error.");
}
} catch (error) {
console.error("Room booking deleting error:", error);
}
};
return (
<div className="App">
<button className="myButton classListButton" onClick={handleClick}>
Classroom list
</button>
{isShown && (
<div className="classList">
{isLoading ? (
<p>Classroom info loading...</p>
) : (
<Dropdown
data={roomData}
dataKey="room"
onChange={handleRoomChange}
selectedRoomId={selectedRoomId}
isLoading={isLoading}
/>
)}
<Calendar
localizer={localizer}
events={events}
startAccessor="start"
endAccessor="end"
onSelectEvent={handleEventClick}
className="cal"
/>
{isDeleting && (
<div className="delete-confirmation">
<p>you sure you want to delete booking?</p>
<button onClick={handleDeleteConfirm}>Yes</button>
<button onClick={handleDeleteCancel}>No</button>
</div>
)}
</div>
)}
<button className="myButton teacherListButton">Create booking</button>
</div>
);
}
export default App;
Dropdown is drop-down list
import React, { useState, useEffect } from 'react';
const Dropdown = ({ data, dataKey, onChange, selectedRoomId, isLoading }) => {
const [options, setOptions] = useState([]);
useEffect(() => {
if (data && data.length > 0) {
setOptions(data.map((item) => ({
value: item.id,
label: item[dataKey],
})));
}
}, [data, dataKey]);
return (
<select value={selectedRoomId} onChange={onChange} disabled={isLoading}>
<option value="">Choose classroom</option>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
export default Dropdown;
The interface consists out of two buttons, "Classroom list" and "create booking." When the first button is pressed, a drop-down list appears where you choose a classroom, and a react big calendar appears where all bookings for the chosen classroom are shown. You can press on a certain date and if there is booking, you will be offered to delete the booking. The "Create booking" button is just a button.
So here's the problem. When I press the first button, it shows a drop-down list and a calendar, but there are no classrooms in the drop-down list. The web console shows this error:
Warning: `value` prop on `select` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.
at select
at Dropdown (http://localhost:3000/static/js/bundle.js:276:3)
at div
at div
at App (http://localhost:3000/static/js/bundle.js:42:80)
I tried consulting a chat gpt, tried to find a problem myself, but it didn't get me far
selectedRoomId
wasnull
. Try looking through the locations where you callsetSelectedRoomId
to see why it got set tonull
.room.json
look like? In theDropdown
component it's redundant to have theoptions
state. Loop overdata
. It will remove one render cycle.