some css fix and websocket fix for item update

This commit is contained in:
2026-02-20 01:38:37 +01:00
parent e7a283b39e
commit aecad87a9a
19 changed files with 997 additions and 633 deletions

View File

@@ -1,13 +1,16 @@
import { HotelProvider, useHotel } from "./HotelContext";
import { HotelProvider, useHotel } from "./context/HotelContext";
import RoomWidget from "./widget/roomWidget"
import ChatWidget from "./widget/chatWidget"
import InventoryWidget from "./widget/inventoryWidget";
import { useEffect, useState } from "react";
import "./MainApp.css"
import AdminWidget from "./widget/adminWidget";
export default function MainAppWrapper({resClientId}) {
const accessToken = localStorage.getItem("access_tokens");
const accessToken = localStorage.getItem("access_tokens");
return (
<HotelProvider accessToken={accessToken} resClientId={resClientId}>
@@ -18,25 +21,29 @@ export default function MainAppWrapper({resClientId}) {
function MainApp() {
const accessToken = localStorage.getItem("access_tokens");
const { rooms, conversations, users } = useHotel();
const VIEW = {
ROOMS: "rooms",
INVENTORY: "inventory",
ADMIN: "admin",
};
const { rooms } = useHotel();
const { conversations } = useHotel();
const { users } = useHotel();
const [view, setView] = useState(VIEW.ROOMS)
return (
<div style={{ padding: "2rem" }}>
<h1>Welcome to MyHotel!</h1>
<div>
<div className="toolbar">
<button className="toolbutton" onClick={() => setView(VIEW.ROOMS)}>Rooms</button>
<button className="toolbutton" onClick={() => setView(VIEW.INVENTORY)}>Inventory</button>
</div>
<h2>Access token</h2>
<pre>{accessToken}</pre>
<h3>Rooms: {rooms.length}</h3>
<h3>Conversations: {conversations.length}</h3>
<h3>Users: {users.length}</h3>
<h2>Dashboard</h2>
<section className="main">
<RoomWidget roomlist={rooms}/>
<ChatWidget convlist={conversations}/>
<InventoryWidget/>
<AdminWidget/>
{view === VIEW.ROOMS && <RoomWidget />}
{view === VIEW.INVENTORY && <InventoryWidget />}
<ChatWidget convlist={conversations} />
</section>
</div>
);

View File

@@ -1,7 +1,23 @@
.main {
display: grid;
grid-template-columns: 70% 30%;
height: 80%;
}
display: flex;
flex-direction: column;
justify-content: space-around;
background-color: rgb(160, 149, 199);
}
.main > * {
min-width: 0; /* prevents overflow bugs */
}
.toolbar {
justify-content: space-around;
height: 10vh;
height: 5vh;
}
.toolbutton {
margin: .5rem;
height: 80%;
width: 20%;
}

View File

@@ -2,7 +2,7 @@ import {createContext, useContext,
useState, useEffect,
useRef
} from "react";
import { API_BASE } from "../config";
import { API_BASE } from "../../config";
const HotelContext = createContext(null);
@@ -16,6 +16,9 @@ export function HotelProvider({ accessToken, children, resClientId}) {
const [rooms, setRooms] = useState([]);
const [conversations, setConversations] = useState([]);
const [messagesByConvId, setMessagesByConvId] = useState({});
const [items, SetItems] = useState([]);
const [users, setUsers] = useState([]);
const [usersById, setUsersById] = useState({});
@@ -55,7 +58,6 @@ export function HotelProvider({ accessToken, children, resClientId}) {
// CHAT
async function createConversation(name) {
const payload = {
@@ -278,11 +280,24 @@ export function HotelProvider({ accessToken, children, resClientId}) {
break;
}
case "item_update": {
SetItems(prev =>
prev.map(i =>
i.id === msg.item_id
? { ...i, amount: msg.number }
: i
)
);
break;
}
case "chat_message": {
appendMessage(msg);
break;
}
default:
console.warn("Unhandled WS message bruuuuh:", msg);
}
@@ -290,7 +305,7 @@ export function HotelProvider({ accessToken, children, resClientId}) {
// --- ADMIN PORTAL ----
function registerUser(username,password,hotel_ids, displayname) {
function registerUser(username, password, hotel_ids, displayname) {
const payload = {
username,
@@ -351,10 +366,11 @@ async function addHotelUser(user_id,hotel_ids) {
if (!accessToken) return;
async function load() {
const [roomsData, convData, usersData] = await Promise.all([
const [roomsData, convData, usersData, itemsData] = await Promise.all([
fetchRooms(),
fetchConversations(),
fetchHotelUsers(),
fetchHotelInventory(),
]);
@@ -362,7 +378,7 @@ async function addHotelUser(user_id,hotel_ids) {
setRooms(roomsData);
setConversations(convData);
setUsers(usersData);
SetItems(itemsData);
//getHotelList();
}
@@ -370,7 +386,7 @@ async function addHotelUser(user_id,hotel_ids) {
load();
//console.log("USERS 2 =",usersById)
console.log("here are hte items" + items)
}, []);
useEffect(() => {
@@ -383,7 +399,8 @@ async function addHotelUser(user_id,hotel_ids) {
//if (!accessTokenSingle || !clientId) return;
const ws = new WebSocket(
`http://localhost:7080/auth/ws/${accessTokenSingle}`
`ws://localhost:7080/auth/ws/${accessTokenSingle}`
//`wss://79.137.75.155/hotel-demo/api/auth/ws/${accessTokenSingle}`
);
wsRef.current = ws;
@@ -423,6 +440,7 @@ async function addHotelUser(user_id,hotel_ids) {
users,
usersById,
clientId,
items,
fetchHotelUsers,
updateRoomStatus,

View File

@@ -0,0 +1,65 @@
import {createContext, useContext,
useState, useEffect,
useRef } from "react";
import { API_BASE } from "../../config";
const RoomContext = createContext(null);
export function UseRoom() {
return useContext(RoomContext);
}
export function RoomProvider({ accessToken, children, resclientid}) {
const [rooms, setRooms] = useState([]);
async function fetchRooms() {
const res = await fetch( `${API_BASE}/rooms/rooms`, {
headers: { Authorization: `Bearer ${accessTokenSingle}` },
});
return res.json();
}
async function updateRoomStatus(roomId, status) {
await fetch(`${API_BASE}/rooms/clean_db_update/${roomId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessTokenSingle}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ status }),
});
// refresh cached rooms
//const updated = await fetchRooms();
//setRooms(updated);
}
//TODO: update room status function
useEffect(() => {
if (!accessToken) return;
async function load() {
const [roomsData] = await Promise.all([
fetchRooms()
]);
setRooms(roomsData);
}
load();
})
return(
<RoomContext.Provider
value={{
rooms,
updateRoomStatus
}}
></RoomContext.Provider>
);
}

View File

@@ -40,22 +40,104 @@ export default function Login({ onLogin }) {
}
}
//TODO: add login button for test1 and test2
async function LoginTest1() {
//e.preventDefault();
//setError("");
const device_id = "147ac10b-58cc-4372-a567-0e02b2c3d479";
let username = "test1"
let password = "test1"
try {
const res = await fetch(`${API_BASE}/auth/create_refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "react-app",
},
credentials: "include",
body: JSON.stringify({ username, password, device_id }),
});
if (!res.ok) {
setError("Invalid credentials or server error.");
return;
}
//const data = await res.json(); // server returns short-lived token
//localStorage.setItem("access_token", data.access_token);
if (onLogin) await onLogin(); // notify App to show MainApp
} catch (err) {
console.error(err);
setError("Network error.");
}
}
async function LoginTest2() {
//e.preventDefault();
//setError("");
const device_id = "147ac10b-58cc-4372-a567-0e02b2c3d479";
let username = "test2"
let password = "test2"
try {
const res = await fetch(`${API_BASE}/auth/create_refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": "react-app",
},
credentials: "include",
body: JSON.stringify({ username, password, device_id }),
});
if (!res.ok) {
setError("Invalid credentials or server error.");
return;
}
//const data = await res.json(); // server returns short-lived token
//localStorage.setItem("access_token", data.access_token);
if (onLogin) await onLogin(); // notify App to show MainApp
} catch (err) {
console.error(err);
setError("Network error.");
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</form>
<>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</form>
<div>
<button onClick={LoginTest1} >login test 1</button>
<button onClick={LoginTest2} >login test 2</button>
</div>
</>
);
}

View File

@@ -1,46 +1,34 @@
.messagebox{
/* Room grid */
.roomGrid {
margin: 5px;
height: 90vh;
overflow-y: auto;
border-radius: 10px;
background-color: var(--card) ;
display: grid;
padding: 16px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
/* Room card */
.card {
background-color: var(--panel) ;
padding: 1rem;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* your messages */
.msg--mine {
align-self: flex-end;
background-color: #464b43;
text-align: right;
}
/* others */
.msg--theirs {
align-self: flex-start;
background-color: #473f3f;
}
.grid {
display: flex;
justify-items: space-around;
gap: 1px;
background-color: #777777;
}
.card {
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 1px;
background-color: #a3a3a3;
padding: 5px;
}
.card input {
width: 90%;
box-sizing: border-box; /* important */
max-width: 100%; /* prevents overflow */
.roomCard input {
width: 100%;
}
.actions {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useHotel } from "../HotelContext";
import { useHotel } from "../context/HotelContext";
export default function AdminWidget() {

View File

@@ -1,16 +1,89 @@
.chatWidget{
display: flex;
flex-direction: column;
}
.convcard {
background-color: brown;
.chatWidget {
margin :5px;
display: flex;
justify-content: space-between;
flex-direction: column;
background: var(--card);
border-radius: 10px;
overflow: hidden;
}
.convlist {
display: flex;
flex-direction: row;
align-items: right;
background-color: darkcyan;
display: flex;
flex-direction: row;
gap: 0.5rem;
padding: 0.75rem;
overflow-y: auto;
height: 20%;
}
.convcard {
padding: 0.5rem 0.75rem;
background: var(--panel);
border-radius: 6px;
cursor: pointer;
}
.convcard:hover {
background: var(--border);
}
.senderBox{
display: flex;
height: 10%;
}
.senderBox * {
border: 0px;
padding: 0px;
}
.textbox{
width: 70%;
}
.sendbutton{
width: 30%;
}
.messageCard {
height: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
background: var(--messageCard);
overflow-y: auto;
}
/* Messages */
.msg {
max-width: 70%;
padding: 0.5rem 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
}
.msg--mine {
align-self: flex-end;
background: var(--accent);
color: white;
}
.msg--theirs {
align-self: flex-start;
background: var(--panel);
}
.createConvBox {
display: flex;
justify-content: space-around;
}
.createConvBox > * {
}

View File

@@ -1,4 +1,5 @@
import { useHotel } from "../HotelContext";
import { useHotel } from "../context/HotelContext";
import { useState, useEffect } from "react";
import { useRef } from "react";
@@ -110,17 +111,63 @@ function SenderBox({ conv_id, onSend, disabled }) {
};
return (
<div hidden={disabled} className="senderbox">
<div hidden={disabled} className="senderBox">
<input
className="textbox"
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === "Enter" && handleSend() }
placeholder="Type a message…"
/>
<button onClick={handleSend}>Send</button>
<button className="sendbutton" onClick={handleSend}>Send</button>
</div>
);
}
function ConvCard({id, title, onOpenConv}) {
return(
<div className="convcard" onClick={() => onOpenConv(id)} >
<h3>ConvId : {id} </h3>
<h3>Name : {title}</h3>
</div>
)
}
function MessagesBox({ conv_id }) {
const { messagesByConvId, usersById, clientId } = useHotel();
const messages = messagesByConvId[conv_id] ?? [];
console.log(messagesByConvId);
console.log(usersById);
if (messages.length === 0) {
return <div className="messagesbox empty">No messages</div>;
}
return (
<div className="messageCard">
{messages
.slice()
.sort((a, b) => new Date(a.sent_at) - new Date(b.sent_at))
.map(msg => {
const isMine = msg.sender_id === clientId;
return (
<div key={msg.sent_at}
className={`msg ${isMine ? "msg--mine" : "msg--theirs"}`}>
<p>
<strong>Sender:</strong>{" "}
{usersById[msg.sender_id] ?? "Unknown user"}
</p>
<p>{msg.content}</p>
</div>
);
})}
</div>
);
}
function AddUsersMenu({ convId, usersById, fetchConvUsers, onValidate, onClose }) {
const [selected, setSelected] = useState(new Set());
@@ -197,53 +244,10 @@ function CreateConvMenu({onSend}) {
<input
value= {nameText}
onChange= {element => setNameText(element.target.value)}
onKeyDown={e => e.key === "Enter" && handleSend() }
placeholder="Ma Conversation"
/>
<button onClick= {handleSend}>Send</button>
</div>
)
}
function ConvCard({id, title, onOpenConv}) {
return(
<div className="convcard">
<h3>ConvId : {id} </h3>
<h3>Name : {title}</h3>
<p onClick={() => onOpenConv(id)}> GET MESSAAGE</p>
</div>
)
}
function MessagesBox({ conv_id }) {
const { messagesByConvId, usersById, clientId } = useHotel();
const messages = messagesByConvId[conv_id] ?? [];
console.log(messagesByConvId);
console.log(usersById);
if (messages.length === 0) {
return <div className="messagesbox empty">No messages</div>;
}
return (
<div className="messagebox">
{messages
.slice()
.sort((a, b) => new Date(a.sent_at) - new Date(b.sent_at))
.map(msg => {
const isMine = msg.sender_id === clientId;
return (
<div key={msg.sent_at}
className={`msg ${isMine ? "msg--mine" : "msg--theirs"}`}>
<p>
<strong>Sender:</strong>{" "}
{usersById[msg.sender_id] ?? "Unknown user"}
</p>
<p>{msg.content}</p>
</div>
);
})}
</div>
);
}

View File

@@ -1,2 +1,23 @@
.grid {
margin: 5px ;
align-items: start;
grid-auto-rows: max-content;
height: 90vh;
overflow-y: auto;
border-radius: 10px;
background-color: var(--card) ;
padding: 16px;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1rem;
display: grid;
}
.itemcard {
background-color: var(--panel) ;
padding: 1rem;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}

View File

@@ -1,54 +1,46 @@
import { useEffect, useState } from "react";
import {HotelProvider, useHotel} from "../HotelContext";
import {HotelProvider, useHotel} from "../context/HotelContext";
import "./inventoryWidget.css";
export default function InventoryWidget({}) {
const {
fetchHotelInventory
} = useHotel();
//const { fetchHotelInventory } = useHotel();
//const [items, setItems] = useState([]);
const [items, setItems] = useState([]);
/*
useEffect(() => {
async function loadInventory() {
const data = await fetchHotelInventory()
setItems(data);
}
loadInventory()
console.log ("loaded inventory")
}, [])
*/
const { items } = useHotel();
return (
<div>
<div className="grid">
{items.map(item => (
<ItemCard
key={item.id}
id={item.id}
name={item.name}
amount={item.amount}
/>
))}
</div>
<div>
<CreateItemMenu/>
</div>
</div>
<div className="grid">
{items.map(item => (
<ItemCard
key={item.id}
id={item.id}
name={item.name}
amount={item.amount}
/>
))}
<CreateItemMenu/>
</div>
</div>
)
}
function ItemCard({id, name, amount}) {
const [itemAmount, setItemAmount] = useState(amount);
const [editing, setEditing] = useState(false);
const {updateItemAmount} = useHotel();
function submit(item_id, item_amount) {
@@ -59,10 +51,12 @@ function ItemCard({id, name, amount}) {
return (
<div className="itemcard">
<div>{name}</div>
<h3>{name}</h3>
{!editing ?(
<p onClick={() => setEditing(true)} /*style={ cursor: "pointer" }*/>Amount :{amount}</p>
<h5 onClick={() => setEditing(true)} >
Amount :{amount}
</h5>
):(
<div>
<input
@@ -74,10 +68,8 @@ function ItemCard({id, name, amount}) {
/>
</div>
)}
</div>
)
}
//TODO: same pb as convlist, update when typing in the create menu
@@ -86,25 +78,23 @@ function CreateItemMenu() {
const [itemAmount, setItemAmount] = useState(0);
const {createItem} = useHotel();
const handleSubmit = () => {
if (!itemName.trim() | !itemAmount.trim() ) return;
createItem(itemName, itemAmount);
}
return(
<div>
<div className="itemcard" >
<input
type="text"
onChange={element => setItemName(element.target.value)}
placeholder="Nom de l'objet"
type="text"
onChange={element => setItemName(element.target.value)}
placeholder="Nom de l'objet"
/>
<input
type="text"
onChange={element => setItemAmount(element.target.value)}
placeholder="Montant d'objet(s)"
type="text"
onChange={element => setItemAmount(element.target.value)}
placeholder="Montant d'objet(s)"
/>
<button onClick={handleSubmit}>Creer</button>
</div>

View File

@@ -1,12 +1,17 @@
import "./RoomWidget.css";
import { useHotel } from "../HotelContext";
import { useHotel } from "../context/HotelContext";
import { useState } from "react";
export default function RoomWidget({ roomlist }) {
export default function RoomWidget({}) {
const { rooms } = useHotel();
return (
<div className="grid">
{roomlist.map(room => (
<div className="roomGrid">
{rooms.map(room => (
<RoomCard
key={room.id}
id = {room.id}
@@ -19,8 +24,6 @@ export default function RoomWidget({ roomlist }) {
}
function RoomCard({ number, status ,id}) {
//FIXME: this shouldn't use hotel context, instead : set the state once on load
const { updateRoomStatus } = useHotel();
const [editing, setEdtiting] = useState(false);