Cleaning up the frontend style.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-10-08 15:13:15 -04:00
parent 85867fab46
commit 06da55443d
10 changed files with 491 additions and 226 deletions

View File

@@ -132,6 +132,10 @@ class Booking(Base):
"""
return self._invitees
def reset_invitee_cache(self) -> None:
"""Reset the cached list of invitee email addresses."""
self._invitee_emails = None
class Invitee(Base):
"""Model representing an invitee to a booking in the application.

View File

@@ -62,6 +62,7 @@ class BookingUpdate(BaseModel):
start_time: datetime | None = None
end_time: datetime | None = None
title: str | None = None
invitees: list[str] | None = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -415,6 +415,7 @@ async def update_booking(
logger.debug(
f"Entering update_booking with booking_id: {booking_id}, params: {kwargs}"
)
try:
# Fetch current booking to compare and validate changes
current = await get_booking(session, booking_id)
@@ -435,6 +436,9 @@ async def update_booking(
# Remove old invitees
await _remove_old_invitees(session, current, new_invitees)
await session.commit() # Ensure deletions are persisted
await session.refresh(current) # Refresh relationship from DB
current.reset_invitee_cache()
# Run validations
logger.debug(
@@ -468,6 +472,16 @@ async def update_booking(
new_invitees,
{i.user_email for i in updated.get_invitee_objects()},
)
await session.commit()
# Always refresh and reset cache after adding invitees
updated = await get_booking(session, booking_id)
await session.refresh(updated)
updated.reset_invitee_cache()
# Force reload of invitee relationship and cache to ensure up-to-date list
# This ensures that updated.invitees returns the correct, current list
_ = [i.user_email for i in updated.get_invitee_objects()]
logger.info(f"Successfully updated booking with id: {booking_id}")

View File

@@ -83,6 +83,7 @@
"eslint-plugin-jest": "29.0.1",
"eslint-plugin-jsdoc": "60.1.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "^6.1.1",
"jest": "30.1.1",
"jest-environment-jsdom": "30.1.1",
"jsdom": "26.1.0",

View File

@@ -10,6 +10,8 @@ import { logger } from "../utils/logger";
// External imports
import React, { useState, useMemo, useCallback } from "react";
// For type usage in JSX event handlers
import {
Box,
Button,
@@ -57,7 +59,7 @@ interface BookingFormProps {
* Called when booking is successful or deleted.
* If booking is provided, navigate to confirmation. If not, just close the form.
*/
onBookingSuccess?: (_booking?: Booking) => void;
onBookingSuccess?: () => void;
}
const BookingForm: React.FC<BookingFormProps> = ({
@@ -298,14 +300,14 @@ const BookingForm: React.FC<BookingFormProps> = ({
title,
invitees,
};
// logger.info("[BookingForm] updateBooking payload:", updatePayload);
logger.info("[BookingForm] updateBooking payload:", updatePayload);
bookingResult = await updateBooking(
Number(editBooking.id),
updatePayload
);
}
if (onBookingSuccess && bookingResult) {
onBookingSuccess(bookingResult);
onBookingSuccess();
}
onClose();
} catch (err: unknown) {
@@ -352,7 +354,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>
{isViewMode
? "View Booking"
? "View Past Booking"
: isEdit
? "Update Booking"
: "New Booking"}
@@ -366,14 +368,46 @@ const BookingForm: React.FC<BookingFormProps> = ({
<DialogContent>
{/* Room selection */}
<Box marginY={2}>
<RoomSelect
selectedRoomId={room_id}
onChange={(id: string | number) =>
handleRoomChange({ target: { value: id } } as SelectChangeEvent)
}
label="Room"
minWidth={180}
/>
{isViewMode ? (
<TextField
label="Room"
value={
currentRoom && currentRoom.name ? currentRoom.name : "\u00A0"
}
fullWidth
margin="normal"
InputProps={{
readOnly: true,
sx: {
color: "rgba(34,34,34,0.8) !important",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
inputProps: {
style: {
color: "rgba(34,34,34,0.8)",
background: "#f0f0f0",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
},
}}
disabled
/>
) : (
<>
<RoomSelect
selectedRoomId={room_id}
onChange={(id: string | number) =>
handleRoomChange({
target: { value: id },
} as SelectChangeEvent)
}
label="Room"
minWidth={180}
/>
</>
)}
<Box color="error.main" fontSize={13}>
{errors.room_id}
</Box>
@@ -381,11 +415,30 @@ const BookingForm: React.FC<BookingFormProps> = ({
{/* Title */}
<TextField
label="Title (optional)"
value={title}
value={isViewMode ? (title ? title : "\u00A0") : title}
onChange={(e) => setTitle(e.target.value)}
fullWidth
margin="normal"
InputProps={{ readOnly: isViewMode }}
InputProps={
isViewMode
? {
readOnly: true,
sx: {
color: "rgba(34,34,34,0.8) !important",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
inputProps: {
style: {
color: "rgba(34,34,34,0.8)",
background: "#f0f0f0",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
},
}
: {}
}
disabled={isViewMode}
/>
{/* Start time */}
@@ -394,11 +447,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
type="datetime-local"
value={(() => {
if (!start) {
return "";
return isViewMode ? "\u00A0" : "";
}
const d = new Date(start);
if (isNaN(d.getTime())) {
return "";
return isViewMode ? "\u00A0" : "";
}
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
@@ -409,8 +462,27 @@ const BookingForm: React.FC<BookingFormProps> = ({
error={!!errors.start}
fullWidth
margin="normal"
required
InputProps={{ readOnly: isViewMode }}
{...(!isViewMode && { required: true })}
InputProps={
isViewMode
? {
readOnly: true,
sx: {
color: "rgba(34,34,34,0.8) !important",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
inputProps: {
style: {
color: "rgba(34,34,34,0.8)",
background: "#f0f0f0",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
},
}
: {}
}
disabled={isViewMode}
/>
<Box color="error.main" fontSize={13} mb={1}>
@@ -422,11 +494,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
type="datetime-local"
value={(() => {
if (!end) {
return "";
return isViewMode ? "\u00A0" : "";
}
const d = new Date(end);
if (isNaN(d.getTime())) {
return "";
return isViewMode ? "\u00A0" : "";
}
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
@@ -437,179 +509,279 @@ const BookingForm: React.FC<BookingFormProps> = ({
error={!!errors.end}
fullWidth
margin="normal"
required
InputProps={{ readOnly: isViewMode }}
{...(!isViewMode && { required: true })}
InputProps={
isViewMode
? {
readOnly: true,
sx: {
color: "rgba(34,34,34,0.8) !important",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
inputProps: {
style: {
color: "rgba(34,34,34,0.8)",
background: "#f0f0f0",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
},
}
: {}
}
disabled={isViewMode}
/>
<Box color="error.main" fontSize={13} mb={1}>
{errors.end}
</Box>
{/* Invitees */}
<FormControl fullWidth margin="normal" error={remainingSlots < 0}>
<InputLabel id="invitees-label">Invitees</InputLabel>
<Select
labelId="invitees-label"
multiple
value={invitees}
onChange={handleInviteeChange}
input={<OutlinedInput label="Invitees" readOnly={isViewMode} />}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{(selected as string[]).map((value, idx) => {
const user = users.find(
(u: import("../interfaces").User) => u.email === value
);
return (
<Chip
key={value}
label={user ? user.name : value}
color={errors[`invitee_${idx}`] ? "error" : undefined}
/>
);
})}
</Box>
)}
disabled={isViewMode}
>
<MenuItem
disabled
sx={{
color: remainingSlots < 0 ? "error.main" : "#222",
fontWeight: 600,
fontSize: 15,
pointerEvents: "none",
background: "inherit",
}}
{isViewMode ? (
<TextField
label="Invitees"
value={
invitees.length === 0
? "\u00A0"
: invitees
.map((value) => {
const user = users.find((u) => u.email === value);
if (user && user.name && user.email) {
return `${user.name} <${user.email}>`;
} else if (user && user.name) {
return user.name;
} else {
return value;
}
})
.join(", ")
}
fullWidth
margin="normal"
InputProps={{
readOnly: true,
sx: {
color: "rgba(34,34,34,0.8) !important",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
inputProps: {
style: {
color: "rgba(34,34,34,0.8)",
background: "#f0f0f0",
opacity: 0.8,
WebkitTextFillColor: "rgba(34,34,34,0.8)",
},
},
}}
disabled
placeholder={invitees.length === 0 ? "No invitees" : undefined}
/>
) : (
<FormControl fullWidth margin="normal" error={remainingSlots < 0}>
<InputLabel
id="invitees-label"
sx={{ backgroundColor: "#fff", opacity: 1, px: 0.5 }}
>
{remainingSlots >= 0
? `${remainingSlots} invitee slot${
remainingSlots === 1 ? "" : "s"
} left`
: `${Math.abs(remainingSlots)} over room capacity`}
</MenuItem>
{users.map((user: import("../interfaces").User) => {
const email = user.email ?? user.name;
const isSelected = invitees.includes(email);
// User unavailable if booked for another event at this time
const unavailable = bookings.some((b: Booking) => {
if (isEdit && b.id === editBooking?.id) {
return false;
Invitees
</InputLabel>
<Select
labelId="invitees-label"
multiple
value={invitees}
onChange={handleInviteeChange}
input={<OutlinedInput label="Invitees" readOnly={isViewMode} />}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{(selected as string[]).map((value, idx) => {
const user = users.find(
(u: import("../interfaces").User) => u.email === value
);
let label;
if (user && user.name && user.email) {
label = `${user.name} <${user.email}>`;
} else if (user && user.name) {
label = user.name;
} else {
label = value;
}
return (
<Chip
key={value}
label={label}
color={errors[`invitee_${idx}`] ? "error" : undefined}
/>
);
})}
</Box>
)}
disabled={isViewMode}
>
<MenuItem
disabled
sx={{
color: remainingSlots < 0 ? "error.main" : "#222",
fontWeight: 600,
fontSize: 15,
pointerEvents: "none",
background: "inherit",
}}
>
{remainingSlots >= 0
? `${remainingSlots} invitee slot${
remainingSlots === 1 ? "" : "s"
} left`
: `${Math.abs(remainingSlots)} over room capacity`}
</MenuItem>
{users.map((user: import("../interfaces").User) => {
const email = user.email ?? user.name;
const isSelected = invitees.includes(email);
// User unavailable if booked for another event at this time
const unavailable = bookings.some((b: Booking) => {
if (isEdit && b.id === editBooking?.id) {
return false;
}
return (
b.invitees?.includes(email) &&
new Date(start) < new Date(b.end_time) &&
new Date(end) > new Date(b.start_time)
);
});
const disableUnselected =
!isSelected &&
(unavailable || invitees.length >= roomCapacity);
let label;
if (user && user.name && user.email) {
label = `${user.name} <${user.email}>`;
} else if (user && user.name) {
label = user.name;
} else {
label = email;
}
return (
b.invitees?.includes(email) &&
new Date(start) < new Date(b.end_time) &&
new Date(end) > new Date(b.start_time)
<MenuItem
key={email}
value={email}
selected={isSelected}
disabled={disableUnselected}
sx={
disableUnselected
? {
color: "#444",
opacity: 1,
fontWeight: "normal",
bgcolor: "inherit",
cursor: "not-allowed",
textDecoration: "line-through",
}
: {
fontWeight: isSelected ? "bold" : "normal",
bgcolor: isSelected
? "rgba(25, 118, 210, 0.08)"
: "inherit",
}
}
>
{isSelected && (
<span
style={{
color: "var(--color-primary)",
marginRight: 8,
}}
>
</span>
)}
{label}
</MenuItem>
);
});
const disableUnselected =
!isSelected &&
(unavailable || invitees.length >= roomCapacity);
return (
<MenuItem
key={email}
value={email}
selected={isSelected}
disabled={disableUnselected}
sx={
disableUnselected
? {
color: "#444",
opacity: 1,
fontWeight: "normal",
bgcolor: "inherit",
cursor: "not-allowed",
textDecoration: "line-through",
}
: {
fontWeight: isSelected ? "bold" : "normal",
bgcolor: isSelected
? "rgba(25, 118, 210, 0.08)"
: "inherit",
}
}
>
{isSelected && (
<span
style={{
color: "var(--color-primary)",
marginRight: 8,
}}
>
</span>
)}
{user.name ? `${user.name} <${email}>` : email}
</MenuItem>
);
})}
</Select>
{errors.invitees && (
<Box color="error.main" fontSize={13} mb={1}>
})}
</Select>
<Box color="error.main" fontSize={13}>
{errors.invitees}
</Box>
)}
</FormControl>
</FormControl>
)}
</DialogContent>
<DialogActions>
{!isViewMode && <Button onClick={onClose}>Cancel</Button>}
{isEdit && !isViewMode && (
{isViewMode ? (
<Button
color="error"
onClick={async () => {
if (editBooking) {
try {
await deleteBooking(editBooking.id);
if (onBookingSuccess) {
onBookingSuccess(); // No booking param: just close
}
onClose();
} catch (err: unknown) {
let errorMsg =
"Failed to delete booking. Please try again.";
// Type guard for error with response
type ErrorWithResponse = {
response?: {
data?: {
detail?: string;
onClick={onClose}
color="primary"
variant="contained"
autoFocus
>
Close
</Button>
) : (
<>
<Button onClick={onClose}>Cancel</Button>
{isEdit && (
<Button
color="error"
onClick={async () => {
if (editBooking) {
try {
await deleteBooking(editBooking.id);
if (onBookingSuccess) {
onBookingSuccess();
}
onClose();
} catch (err: unknown) {
let errorMsg =
"Failed to delete booking. Please try again.";
type ErrorWithResponse = {
response?: {
data?: {
detail?: string;
message?: string;
};
};
message?: string;
};
};
message?: string;
};
const errorObj = err as ErrorWithResponse;
if (
errorObj &&
typeof errorObj === "object" &&
errorObj.response &&
typeof errorObj.response === "object" &&
errorObj.response.data &&
typeof errorObj.response.data === "object"
) {
const data = errorObj.response.data;
if (typeof data.detail === "string") {
errorMsg = data.detail;
} else if (typeof data.message === "string") {
errorMsg = data.message;
const errorObj = err as ErrorWithResponse;
if (
errorObj &&
typeof errorObj === "object" &&
errorObj.response &&
typeof errorObj.response === "object" &&
errorObj.response.data &&
typeof errorObj.response.data === "object"
) {
const data = errorObj.response.data;
if (typeof data.detail === "string") {
errorMsg = data.detail;
} else if (typeof data.message === "string") {
errorMsg = data.message;
}
} else if (
errorObj &&
typeof errorObj === "object" &&
typeof errorObj.message === "string"
) {
errorMsg = errorObj.message ?? errorMsg;
}
logger.error(
"BookingForm: Error deleting booking",
err
);
setErrors({ submit: errorMsg });
}
} else if (
errorObj &&
typeof errorObj === "object" &&
typeof errorObj.message === "string"
) {
errorMsg = errorObj.message ?? errorMsg;
}
logger.error("BookingForm: Error deleting booking", err);
setErrors({ submit: errorMsg });
}
}
}}
>
Delete
</Button>
}}
>
Delete
</Button>
)}
<Button
type="submit"
variant="contained"
disabled={isFormError}
color="primary"
>
{isEdit ? "Update" : "Book"}
</Button>
</>
)}
<Button type="submit" variant="contained" disabled={isFormError}>
{isViewMode ? "Close" : isEdit ? "Update" : "Book"}
</Button>
</DialogActions>
</form>
</Dialog>

View File

@@ -45,9 +45,25 @@ const RoomSelect: React.FC<RoomSelectProps> = ({
// [RoomSelect] Unmounted log removed
};
}, [rooms.length]);
// Compute background color for selected room
let selectBgColor: string | undefined = undefined;
if (rooms.length > 0 && selectedRoomId) {
const selectedRoom = rooms.find(
(room) => String(room.id) === String(selectedRoomId)
);
if (selectedRoom) {
const roomIdx = ((Number(selectedRoom.id) - 1) % 20) + 1;
selectBgColor = `var(--room-color-${roomIdx})`;
}
}
return (
<FormControl size="small" sx={{ minWidth }}>
<InputLabel id="room-select-label">{label}</InputLabel>
<InputLabel
id="room-select-label"
sx={{ backgroundColor: "#fff", opacity: 1, px: 0.5 }}
>
{label}
</InputLabel>
<Select
labelId="room-select-label"
value={selectedRoomId}
@@ -56,6 +72,11 @@ const RoomSelect: React.FC<RoomSelectProps> = ({
logger.info(`[RoomSelect] Room changed to ${e.target.value}`);
onChange(e.target.value as string | number);
}}
sx={{
backgroundColor: selectBgColor,
color: "#fff",
transition: "background-color 0.2s",
}}
>
{rooms.map((room) => {
// Compute room index directly from room.id

View File

@@ -21,8 +21,8 @@ import type { Booking } from "../interfaces";
interface BookingContextType {
bookings: Booking[];
/* eslint-disable-next-line no-unused-vars */
fetchMonth: (month: string) => Promise<void>;
refreshBookings: () => Promise<void>;
}
export const BookingContext = createContext<BookingContextType | undefined>(
@@ -53,14 +53,24 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
// Flat array for UI filtering
const bookings = Object.values(bookingsByMonth).flat();
// Initial load: fetch all bookings for all rooms for today
useEffect(() => {
// On mount, fetch current month
// Helper to get current month string
const getCurrentMonthStr = () => {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, "0");
const monthStr = `${yyyy}-${mm}`;
fetchMonth(monthStr);
return `${yyyy}-${mm}`;
};
// Refresh bookings for current month
const refreshBookings = async () => {
const monthStr = getCurrentMonthStr();
await fetchMonth(monthStr);
};
// Initial load: fetch current month
useEffect(() => {
refreshBookings();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// SSE subscription for live updates
@@ -175,7 +185,7 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
}, []);
return (
<BookingContext.Provider value={{ bookings, fetchMonth }}>
<BookingContext.Provider value={{ bookings, fetchMonth, refreshBookings }}>
{children}
</BookingContext.Provider>
);

View File

@@ -332,18 +332,14 @@ const BookingPage: React.FC = () => {
setFormOpen(true);
};
const refetchBookings = async () => {
// Bookings are now managed by BookingContext; context handles refresh
};
const navigate = useNavigate();
const { refreshBookings } = useBookings();
const handleFormClose = (refresh = false, booking?: Booking) => {
// logger.info("[BookingPage] Booking form closed", { refresh, booking });
setFormOpen(false);
setFormSlot(null);
setEditBooking(null);
if (refresh) {
refetchBookings();
if (refresh && typeof refreshBookings === "function") {
refreshBookings();
}
if (booking) {
navigate("/confirmation", { state: { booking } });

View File

@@ -44,12 +44,13 @@ import type { Booking } from "../interfaces";
const ConfirmationPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
// Always call hooks at the top level
const { bookings, refreshBookings } = useBookings();
// booking is passed from BookingForm or BookingPage
/**
* Booking state, updated via SSE for live status.
*/
const { bookings } = useBookings();
const [booking, setBooking] = useState<Booking | null>(
location.state?.booking || null
);
@@ -135,7 +136,7 @@ const ConfirmationPage: React.FC = () => {
* Fetch all rooms for editing.
*/
const {
// ...existing code...
data: allRooms = [],
isLoading: roomsLoading,
error: roomsError,
} = useQuery({
@@ -158,11 +159,25 @@ const ConfirmationPage: React.FC = () => {
const handleBack = () => navigate("/booking");
const handleFormClose = (refresh = false) => {
setEditing(false);
if (refresh) {
// Optionally refetch booking data here
if (refresh && typeof refreshBookings === "function") {
refreshBookings();
}
};
// Enrich booking with room details if missing
let enrichedBooking = booking;
if (
enrichedBooking &&
!enrichedBooking.room &&
enrichedBooking.room_id &&
Array.isArray(allRooms)
) {
const foundRoom = allRooms.find((r) => r.id === enrichedBooking.room_id);
if (foundRoom) {
enrichedBooking = { ...enrichedBooking, room: { name: foundRoom.name } };
}
}
return (
<Box
sx={{
@@ -186,16 +201,10 @@ const ConfirmationPage: React.FC = () => {
}}
onBookingSuccess={(resultBooking?: Booking) => {
if (!resultBooking) {
// Deleted
// logger.info("[ConfirmationPage] Booking deleted", booking);
setDeleted(true);
setEditing(false);
setBooking(null);
} else {
// logger.info(
// "[ConfirmationPage] Booking success (created or edited)",
// resultBooking
// );
setEditing(false);
setBooking(resultBooking);
}
@@ -203,7 +212,7 @@ const ConfirmationPage: React.FC = () => {
/>
) : (
<BookingConfirmation
booking={booking}
booking={enrichedBooking}
deleted={deleted}
onEdit={handleEdit}
onBack={handleBack}

View File

@@ -53,7 +53,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04"
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.24.4", "@babel/core@^7.27.4", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496"
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
@@ -245,7 +245,7 @@
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.4"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.24.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8"
integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==
@@ -4084,16 +4084,16 @@
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
version "5.0.7"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6"
integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b"
integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/send" "*"
"@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33":
"@types/express-serve-static-core@^4.17.21":
version "4.19.6"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267"
integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==
@@ -4103,6 +4103,16 @@
"@types/range-parser" "*"
"@types/send" "*"
"@types/express-serve-static-core@^4.17.33":
version "4.19.7"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz#f1d306dcc03b1aafbfb6b4fe684cce8a31cffc10"
integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/send" "*"
"@types/express@*":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956"
@@ -4238,11 +4248,11 @@
"@types/node" "*"
"@types/node@*":
version "24.6.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.6.2.tgz#59b99878b6fed17e698e7d09e51c729c5877736a"
integrity sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==
version "24.7.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.0.tgz#a34c9f0d3401db396782e440317dd5d8373c286f"
integrity sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==
dependencies:
undici-types "~7.13.0"
undici-types "~7.14.0"
"@types/node@16.7.13":
version "16.7.13"
@@ -4332,7 +4342,7 @@
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
"@types/react@*", "@types/react@>=16":
"@types/react@*":
version "19.2.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.0.tgz#8412946e7e1efb0de9bb59b3aa87676d96add385"
integrity sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==
@@ -4346,6 +4356,13 @@
dependencies:
csstype "^3.0.2"
"@types/react@>=16":
version "19.2.2"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
dependencies:
csstype "^3.0.2"
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -5576,9 +5593,9 @@ base64-js@^1.3.1:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.8.9:
version "2.8.12"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz#7cb875f4c5b5ab4528109df277b2f0e1971ba27e"
integrity sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==
version "2.8.13"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz#3d49a18ee27114765401f4985bdc27018603854e"
integrity sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==
batch@0.6.1:
version "0.6.1"
@@ -5834,9 +5851,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746:
version "1.0.30001747"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz#2cfbbb7f1f046439ebaf34bba337ee3d3474c7e5"
integrity sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==
version "1.0.30001748"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz#628a5a9293014e58f8ba1216bb4966b04c58bee0"
integrity sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==
case-sensitive-paths-webpack-plugin@^2.4.0:
version "2.4.0"
@@ -6906,9 +6923,9 @@ ejs@^3.1.6, ejs@^3.1.8:
jake "^10.8.5"
electron-to-chromium@^1.5.227:
version "1.5.230"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz#06ddb4a6302a78b2a3e8dcf1dd2563bcfdd546c9"
integrity sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==
version "1.5.232"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz#3de180ee54c14c58d56a290f588eef3a934ebda6"
integrity sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==
emittery@^0.10.2:
version "0.10.2"
@@ -7352,6 +7369,16 @@ eslint-plugin-react-hooks@^4.3.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596"
integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
eslint-plugin-react-hooks@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz#c04c051e382444bb62cdca9cf4df68ffe0845b8a"
integrity sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==
dependencies:
"@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4"
zod "^3.22.4 || ^4.0.0"
zod-validation-error "^3.0.3 || ^4.0.0"
eslint-plugin-react@7.37.5, eslint-plugin-react@^7.27.1:
version "7.37.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
@@ -10583,9 +10610,9 @@ map-or-similar@^1.5.0:
integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
markdown-to-jsx@^7.1.8:
version "7.7.13"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz#8144d89e396cdf3264bec7c97a21bc47cfc0773b"
integrity sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==
version "7.7.15"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.15.tgz#7dad08e2b2cf35460a1c157f5d3e7bc875bcb511"
integrity sha512-U5dw5oRajrPTE2oJQWAbLK8RgbCDJ264AjW3fGABq+/rZjQ0E/WGVCLKAHvpKHQFUwoWoK8ZZWVPNLR/biYMhg==
math-intrinsics@^1.1.0:
version "1.1.0"
@@ -14256,10 +14283,10 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici-types@~7.13.0:
version "7.13.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.13.0.tgz#a20ba7c0a2be0c97bd55c308069d29d167466bff"
integrity sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==
undici-types@~7.14.0:
version "7.14.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840"
integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.1"
@@ -15278,3 +15305,13 @@ yocto-queue@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.1.tgz#36d7c4739f775b3cbc28e6136e21aa057adec418"
integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==
"zod-validation-error@^3.0.3 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
"zod@^3.22.4 || ^4.0.0":
version "4.1.12"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==