Compare commits

...

7 Commits

Author SHA1 Message Date
Victor
05b0a4514b changement test items 2024-05-13 21:23:40 +02:00
560226b6a8 update README 2024-05-13 19:54:18 +02:00
4e6a03df17 room creation fix + README update 2024-05-13 19:53:43 +02:00
22ec7f8fdd register + deletions + image request changes 2024-05-13 19:42:49 +02:00
baf31f7959 update rooms 2024-05-13 18:12:59 +02:00
1e3dcf340d commit final 2024-05-13 01:42:15 +02:00
Victor
6506d6817b fix date reset 2024-05-13 00:59:33 +02:00
32 changed files with 994 additions and 356 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,14 @@
"", "",
"\\src", "\\src",
"\\src\\api", "\\src\\api",
"\\src\\hooks", "\\src\\assets",
"\\src\\pages", "\\src\\components",
"\\tests-examples", "\\src\\components\\form",
"\\__tests__", "\\src\\components\\item",
"\\__tests__\\api", "\\src\\components\\nav",
"\\__tests__\\pages", "\\src\\components\\parts",
"\\__tests__\\pages\\authenticated" "\\src\\components\\rooms"
], ],
"SelectedNode": "\\src\\pages\\rooms-stats.jsx", "SelectedNode": "\\README.md",
"PreviewInSolutionExplorer": false "PreviewInSolutionExplorer": false
} }

Binary file not shown.

View File

@@ -11,6 +11,25 @@ Victor DESCAMPS
the tests sheet 'api/image-request.test.jsx' was commented out to avoid over-sending requests to the google API during development. Remember to uncomment the file if you want to run all the tests once, but remember to recomment the file if you want to run a series of tests. the tests sheet 'api/image-request.test.jsx' was commented out to avoid over-sending requests to the google API during development. Remember to uncomment the file if you want to run all the tests once, but remember to recomment the file if you want to run a series of tests.
An over-sending of requests could get us blocked for the day by google. An over-sending of requests could get us blocked for the day by google.
## Project summary (FR):
Nous n'avons pas r<>ussi <20> mettre en <20>uvre toutes les fonctionnalit<69>s de l'interface utilisateur <20> temps. Le d<>veloppement n'a pas non plus <20>t<EFBFBD> rigoureusement test-driven.
r<EFBFBD>sum<EFBFBD> des fonctionnalit<69>s :
L'utilisateur peut cr<63>er un compte sur la page de login (navbar)
En se connectant, l'utilisateur peut charger la liste des articles et les filtrer par pi<70>ce, date, nom, etc. Il est possible de cr<63>er, supprimer et de mettre <20> jour des articles.
Une page permet de consulter les statistiques des pi<70>ces et la liste des pi<70>ces. Il est possible d'y cr<63>er de nouvelles, de les modifier ou de les supprimer (supprimant <20>galement les articles li<6C>s).
Toute op<6F>ration d'<27>criture r<>ussie provoque un rechargement de page.
Les icones AntDesign provoquent syst<73>matiquement une exception dans la console. Nous n'avons pas <20>t<EFBFBD> en mesure de corriger ou <20>viter cette erreur. Les icones s'affichent bien, donc <20>a ne posera pas de probl<62>me <20> part la pollution de la console.
Nous avons voulu personnaliser un peu notre projet pour le diff<66>rencier des autres, nous avons donc mis en place un syst<73>me simple de recherche automatique d'images li<6C>es aux objets enregistr<74>s, et une requ<71>te <20> l'API Inspirobot <20> chaque rechargement de la page sur la page d'accueil pour un accueil chaleureux.
Nous sommes conscients que ce projet ne correspond pas exactement aux attentes d'un d<>veloppement full test-driven. Mais nous nous satisfaisons tout de m<>me de nous <20>tre familiaris<69>s avec js et React, avec les tests en g<>n<EFBFBD>ral, et d'avoir pris du plaisir <20> d<>velopper ce petit projet.
Nous vous souhaitons une bonne correction et vous remercions pour vos cours.
## Run Locally ## Run Locally

View File

@@ -1,35 +1,78 @@
/*import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
import { render, fireEvent, screen, cleanup } from "@testing-library/react"; import { render, fireEvent, screen, cleanup } from "@testing-library/react";
import { Login } from "../../../src/pages/index.js"; import { ItemPage } from "../../../src/components/item/ItemPage.jsx"; // Assurez-vous d'importer correctement votre composant
import { AuthenticationContext } from "../../../src/contexts/index.js"; import { AuthenticationContext } from "../../../src/contexts/index.js";
describe('Items Page', () => { describe("Item Page with Authentication", () => {
let rooms;
beforeEach(() => { beforeEach(() => {
// Se connecter à l'application avec le compte pseudo : Juvisi!0TEST et mdp : Juvisi!0TEST // Simuler la connexion de l'utilisateur
cy.visit('/'); const user = { username: "Juvisi!0TEST", password: "Juvisi!0TEST" };
cy.findByPlaceholderText('username').type('Juvisi!0TEST'); const loginFunction = vi.fn().mockResolvedValue(user); // Mock de la fonction de connexion
cy.findByPlaceholderText('password').type('Juvisi!0TEST'); rooms = [{ _id: "66425c9d8869396c3e853633", name: "test" }];
cy.findByText('submit').click(); render(
// Vérifier que la connexion est réussie et que vous êtes redirigé vers la page '/items' <AuthenticationContext.Provider value={{ login: loginFunction }}>
cy.findByText('Welcome Juvisi!0TEST').should('exist'); <ItemPage />
</AuthenticationContext.Provider>,
);
}); });
it('creates an item', () => { afterEach(cleanup); // Nettoyer après chaque test pour éviter les fuites de mémoire
// Créer un nouvel item
cy.findByPlaceholderText('Item name').type('New Item'); it("renders without errors", () => {
cy.findByText('Add Item').click(); expect(screen.getByTestId("item-container")).toBeTruthy();
}); });
it('verifies the item is created', () => { /*it('allows filtering items', async () => {
// Vérifier que l'élément est ajouté à la liste // Simuler une saisie dans le champ de recherche par nom
cy.findByText('New Item').should('exist'); fireEvent.change(screen.getByLabelText('Search by name:'), { target: { value: 'Item' } });
}); // Attendre que les éléments filtrés soient rendus dans la liste
await screen.findByText('Filtered Item');
});*/
it('deletes the item', () => { /*it('allows creating an item', async () => {
// Supprimer l'élément // Simuler un clic sur le bouton "Create Item"
cy.findByText('New Item').siblings('button').click(); fireEvent.click(screen.getByTestId('create-item-button'));
}); // Attendre que le modal de création d'élément s'ouvre
}); await screen.findByTestId('create-item-button');
// Simuler une saisie dans les champs du formulaire de création d'élément
fireEvent.change(screen.getByTestId("create-item-Brand-Input"), { target: { value: 'Brand X' } });
fireEvent.change(screen.getByTestId("create-item-Model-Input"), { target: { value: 'Model Y' } });
// Simuler un clic pour ouvrir le menu déroulant
fireEvent.mouseDown(screen.getByTestId("create-item-Room-Select"));
// Attendre que les options du menu déroulant soient rendues
await screen.findByText('test');
// Simuler un clic pour choisir l'option "test"
fireEvent.click(screen.getByText('test'));
*/ //impossible de tester car element pas encore terminé // Simuler un clic sur le bouton "Submit" du formulaire
fireEvent.click(screen.getByText('Submit'));
// Attendre que l'élément soit ajouté à la liste
await screen.findByText('Brand X');
await screen.findByText('Model Y');
});*/
/*it('allows updating an item', async () => {
// Simuler un clic sur le bouton "Edit" de l'élément à mettre à jour
fireEvent.click(screen.getByText('Edit'));
// Attendre que le modal de mise à jour de l'élément s'ouvre
await screen.findByText('Update Item');
// Simuler une saisie dans les champs du formulaire de mise à jour d'élément
fireEvent.change(screen.getByLabelText('Brand'), { target: { value: 'Updated Brand' } });
fireEvent.change(screen.getByLabelText('Model'), { target: { value: 'Updated Model' } });
// Simuler un clic sur le bouton "Submit" du formulaire de mise à jour
fireEvent.click(screen.getByText('Submit'));
// Attendre que l'élément mis à jour soit affiché dans la liste
await screen.findByText('Updated Brand Updated Model');
});*/
/*it('allows deleting an item', async () => {
// Simuler un clic sur le bouton "Delete" de l'élément à supprimer
fireEvent.click(screen.getByText('Delete'));
// Attendre que l'élément soit supprimé de la liste
await screen.findByText('Updated Brand Updated Model', { timeout: 5000, shouldNotExist: true });
});*/
});

View File

@@ -24,3 +24,45 @@ export const getItems = async () => {
return error.response.data; return error.response.data;
} }
}; };
export const createItem = async (values) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_API_URL}/item`,
values,
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}
export const updateItem = async (_id, values) => {
try {
const response = await axios.put(
`${import.meta.env.VITE_API_URL}/item/${_id}`,
values,
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}
export const deleteItem = async (_id) => {
try {
const response = await axios.delete(
`${import.meta.env.VITE_API_URL}/item/${_id}`
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}

View File

@@ -5,7 +5,8 @@ import axios from "axios";
export const getRoom = async (_id) => { export const getRoom = async (_id) => {
try { try {
const response = await axios.get("/room/"+{_id}); let id = _id.toString()
const response = await axios.get(`/room/${id}`);
console.log(response.data) console.log(response.data)
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -25,6 +26,56 @@ export const getRooms = async () => {
} }
}; };
export const createRoom = async (values) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_API_URL}/room`,
values,
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}
export const updateRoom = async(_id, values) => {
try {
const response = await axios.put(
`${import.meta.env.VITE_API_URL}/room/${_id}`,
values,
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}
export const deleteRoom = async (_id) => {
try {
const itemsResponse = await axios.get(
`${import.meta.env.VITE_API_URL}/item`,
);
const items = itemsResponse.data;
items?.forEach(async (item) => {
await axios.delete(
`${import.meta.env.VITE_API_URL}/item/${item._id}`,
);
})
const response = await axios.delete(
`${import.meta.env.VITE_API_URL}/room/${_id}`
);
console.log(response.data);
return response;
} catch (error) {
console.error(error);
}
}
export const getRoomStats = async () => { export const getRoomStats = async () => {
try { try {
const response = await axios.get("/room/stats"); const response = await axios.get("/room/stats");
@@ -57,7 +108,7 @@ function formatRoomStatsGlobal(global){
const most_expensive_room = { const most_expensive_room = {
name: global.most_expensive_room.name, name: global.most_expensive_room.name,
price: global.most_expensive_room.price count: global.most_expensive_room.count
}; };
return { return {

View File

@@ -1,11 +1,11 @@
.diagram-container { .diagram-container {
width: 100%; /* Utilise toute la largeur horizontale disponible */ width: 100%;
margin-bottom: 20px; margin-bottom: 20px;
} }
.row-container { .row-container {
display: flex; display: flex;
align-items: flex-end; /* Alignement des <20>l<EFBFBD>ments <20> la base */ align-items: flex-end;
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -20,9 +20,9 @@
.value { .value {
position: absolute; position: absolute;
bottom: 50%; /* Alignement par rapport <20> la base de l'<27>l<EFBFBD>ment parent */ bottom: 50%;
right: 0; right: 0;
transform: translate(50%, 50%); /* Ajustement de la position */ transform: translate(50%, 50%);
color: white; color: white;
font-size: 12px; font-size: 12px;
padding: 5px; padding: 5px;
@@ -35,6 +35,6 @@
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
color: #333; color: #333;
position: relative; /* Permet l'alignement des labels <20> la base */ position: relative;
bottom: 0; /* Alignement <20> la base */ bottom: 0;
} }

View File

@@ -78,8 +78,7 @@
} }
.item-list { .item-list {
width: 100%; width: 50vw;
max-width:50vw;
} }
.pagination { .pagination {
@@ -87,12 +86,10 @@
} }
/* Style pour les boutons de pagination */
.pagination button { .pagination button {
margin: 0 5px; margin: 0 5px;
} }
/* Style pour les ItemBox */
.item-container .item-list .ant-col { .item-container .item-list .ant-col {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -9,7 +9,7 @@
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
margin-bottom: 20px; margin-bottom: 20px;
width: 250px; /* Largeur ajustable en fonction de votre mise en page */ width: 250px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
@@ -57,3 +57,34 @@
.product-details button:hover { .product-details button:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.button-group {
margin-top: 10px;
display: flex;
}
.edit-button,
.delete-button {
flex: 1;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.edit-button {
background-color: #007bff;
color: #fff;
margin-right: 10px;
}
.delete-button {
background-color: #dc3545;
color: #fff;
}
.edit-button:hover,
.delete-button:hover {
background-color: #0056b3;
}

View File

@@ -1,4 +1,3 @@
/* CSS pour la fen<65>tre modale */
.modal { .modal {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@@ -1,22 +1,22 @@
.list-container { .list-container {
width: 100%; width: 100%;
max-height:500px; max-height:500px;
overflow-y: auto; /* Activer le d<>filement vertical si n<>cessaire */ overflow-y: auto;
padding: 20px; /* Espace int<6E>rieur de la liste */ padding: 20px;
} }
.room-details { .room-details {
width: 100%; /* Largeur de chaque bo<62>te */ width: 100%;
background-color: #f9f9f9; background-color: #f9f9f9;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px; padding: 20px;
margin-bottom: 20px; /* Espace entre chaque bo<62>te */ margin-bottom: 20px;
transition: transform 0.3s ease; transition: transform 0.3s ease;
} }
.room-details:hover { .room-details:hover {
transform: translateY(-5px); /* Effet d'<27>l<EFBFBD>vation au survol */ transform: translateY(-5px);
} }
.room-details .title { .room-details .title {

View File

@@ -1,7 +1,8 @@
import React from "react"; import React, { useContext } from "react";
import { AuthenticationProvider } from "../contexts"; import { AuthenticationProvider } from "../contexts";
export const Authenticated = ({ children }) => { export const Authenticated = ({ children }) => {
return <AuthenticationProvider>{children}</AuthenticationProvider>; return <AuthenticationProvider>{children}</AuthenticationProvider>;
}; };

View File

@@ -15,8 +15,8 @@ const Diagram = ({ data }) => {
<div className="diagram-container"> <div className="diagram-container">
<h2>Prix total par an</h2> <h2>Prix total par an</h2>
{diagramData.map(({ name, value }) => { {diagramData.map(({ name, value }) => {
// Appliquer l'<27>chelle logarithmique <EFBFBD> la valeur // Appliquer l'<27>chelle logarithmique sur la valeur
const scaledValue = Math.log(value + 1); // Ajouter 1 pour <20>viter le logarithme de z<>ro const scaledValue = Math.log(value + 1);
const scaledMaxValue = Math.log(maxValue + 1); const scaledMaxValue = Math.log(maxValue + 1);
return ( return (
<div key={name} className="row-container"> <div key={name} className="row-container">

View File

@@ -1,6 +1,9 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd"; import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd";
import axios from "axios"; import axios from "axios";
import { createItem } from '../../api/item'
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
@@ -26,16 +29,9 @@ export const FormCreateItem = ({ onClose }) => {
}, []); }, []);
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await createItem(values);
const response = await axios.post( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/item`, window.location.reload();
values,
);
console.log(response.data);
// Fermer la fenêtre modale après soumission réussie
onClose();
} catch (error) {
console.error(error);
} }
}; };

View File

@@ -1,19 +1,16 @@
import React from "react"; import React from "react";
import { Form, Input, Button } from "antd"; import { Form, Input, Button } from "antd";
import { createRoom } from '../../api/room'
import axios from "axios"; import axios from "axios";
export const FormCreateRoom = () => { export const FormCreateRoom = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await createRoom(values);
const response = await axios.post( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/room`, window.location.reload();
values,
);
console.log(response.data);
} catch (error) {
console.error(error);
} }
}; };

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd"; import { Form, Input, InputNumber, Button, Select, DatePicker } from "antd";
import axios from "axios"; import axios from "axios";
import { getRooms } from "../../api/room"; import { getRooms } from "../../api/room";
import { getItem } from "../../api/item"; import { getItem, updateItem } from "../../api/item";
import moment from "moment"; import moment from "moment";
const { Option } = Select; const { Option } = Select;
@@ -69,16 +69,9 @@ export const FormUpdateItem = ({ itemId, onClose }) => {
}, [item, form]); }, [item, form]);
const onFinish = async (values) => { const onFinish = async (values) => {
try { let response = await updateItem(item._id, values);
const response = await axios.put( if (response?.status >= 200 && response?.status < 300) {
`${import.meta.env.VITE_API_URL}/item/${item._id}`, window.location.reload();
values,
);
console.log(response.data);
// Fermer la fenêtre modale après soumission réussie
onClose();
} catch (error) {
console.error(error);
} }
}; };
@@ -87,7 +80,7 @@ export const FormUpdateItem = ({ itemId, onClose }) => {
<Form <Form
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
initialValues={item} // Initialise le formulaire avec les valeurs de l'élément initialValues={item}
> >
<h1>Update Item</h1> <h1>Update Item</h1>
<Form.Item label="Brand" name="brand"> <Form.Item label="Brand" name="brand">

View File

@@ -0,0 +1,62 @@
import React, { useState, useEffect } from "react";
import {getRoom, updateRoom, deleteRoom } from '../../api/room'
import { Form, Input, Button } from "antd";
import axios from "axios";
export const FormUpdateRoom = ({_id}) => {
const [form] = Form.useForm();
const [room, setRoom] = useState();
useEffect(() => {
const fetchData = async () => {
console.log(_id)
const roomsResponse = await getRoom(_id);
setRoom(roomsResponse);
console.log(roomsResponse);
};
fetchData();
}, [_id]);
useEffect(() => {
if (room) {
form.setFieldsValue({
...room
});
}
}, [room, form]);
const onFinish = async (values) => {
let response = await updateRoom(_id, values);
if (response?.status >= 200 && response?.status < 300) {
window.location.reload();
}
};
const onDelete = async () => {
let response = await deleteRoom(_id);
if (response?.status >= 200 && response?.status < 300) {
window.location.reload();
}
}
return (
<Form form={form} onFinish={onFinish}>
<button onClick={onDelete}> Supprimer </button>
<Form.Item
label="Room Name"
name="name"
rules={[{ required: true, message: "Please input the room name!" }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default FormUpdateRoom;

View File

@@ -5,21 +5,27 @@ import FormUpdateItem from "../form/formUpdateItem";
import { Image } from "../parts/image"; import { Image } from "../parts/image";
import { Description } from "../parts/description"; import { Description } from "../parts/description";
import { Characteristic } from "../parts/characteristic"; import { Characteristic } from "../parts/characteristic";
import { deleteItem } from '../../api/item'
// Composant Détails du Produit
export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => { export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
// Fonction pour ouvrir la fenêtre modale
const openModal = () => { const openModal = () => {
setIsModalOpen(true); setIsModalOpen(true);
}; };
// Fonction pour fermer la fenêtre modale
const closeModal = () => { const closeModal = () => {
setIsModalOpen(false); setIsModalOpen(false);
}; };
const onDelete = async () => {
let response = await deleteItem(_id);
if (response?.status >= 200 && response?.status < 300) {
window.location.reload();
}
}
let productname = brand + " " + model; let productname = brand + " " + model;
let formatedPrice = price + "€"; let formatedPrice = price + "€";
let formatedDate = new Date(purchaseDate).toLocaleDateString("fr-FR"); let formatedDate = new Date(purchaseDate).toLocaleDateString("fr-FR");
@@ -29,13 +35,16 @@ export const ItemBox = ({ model, brand, purchaseDate, price, _id }) => {
return ( return (
<div className="product-details"> <div className="product-details">
<Description title={productname}> <Description title={productname}>
<Image request={request} _id={_id} alt="Product" /> <Image request={request} alt="Product" />
<Characteristic label="Model" value={model} /> <Characteristic label="Model" value={model} />
<Characteristic label="Brand" value={brand} /> <Characteristic label="Brand" value={brand} />
<Characteristic label="Purchase Date" value={formatedDate} /> <Characteristic label="Purchase Date" value={formatedDate} />
<Characteristic label="Price" value={formatedPrice} /> <Characteristic label="Price" value={formatedPrice} />
{/* Bouton d'édition pour ouvrir la fenêtre modale */} {/* Boutons d'édition et de suppression */}
<button onClick={openModal}>Edit</button> <div className="button-group">
<button className="edit-button" onClick={openModal}>Edit</button>
<button className="delete-button" onClick={onDelete}>Delete</button>
</div>
</Description> </Description>
{/* Fenêtre modale */} {/* Fenêtre modale */}
{isModalOpen && ( {isModalOpen && (

View File

@@ -68,9 +68,13 @@ export const ItemPage = () => {
(filterPriceMin === "" || item.price >= parseFloat(filterPriceMin)) && (filterPriceMin === "" || item.price >= parseFloat(filterPriceMin)) &&
(filterPriceMax === "" || item.price <= parseFloat(filterPriceMax)); (filterPriceMax === "" || item.price <= parseFloat(filterPriceMax));
const matchesDate = const matchesDate =
!filterDateRange ||
filterDateRange.length === 0 || filterDateRange.length === 0 ||
(new Date(item.purchaseDate) >= filterDateRange[0] && (filterDateRange[0] &&
filterDateRange[1] &&
new Date(item.purchaseDate) >= filterDateRange[0] &&
new Date(item.purchaseDate) <= filterDateRange[1]); new Date(item.purchaseDate) <= filterDateRange[1]);
return matchesRoom && matchesName && matchesPrice && matchesDate; return matchesRoom && matchesName && matchesPrice && matchesDate;
}); });
setFilteredItems(filtered); setFilteredItems(filtered);
@@ -106,7 +110,55 @@ export const ItemPage = () => {
return ( return (
<div className="item-container"> <div className="item-container">
<div className="filters">{/* Filters here */}</div> <div className="filters">
<div className="filter-group">
<label htmlFor="roomSelect">Room:</label>
<Select
id="roomSelect"
defaultValue="all"
style={{ width: 120 }}
onChange={handleRoomChange}
>
<Option value="all">All</Option>
{rooms.map((room) => (
<Option key={room._id} value={room._id}>
{room.name}
</Option>
))}
</Select>
</div>
<div className="filter-group">
<label htmlFor="nameInput">Search by name:</label>
<Input
id="nameInput"
placeholder="Search by name"
value={filterName}
onChange={(e) => setFilterName(e.target.value)}
/>
</div>
<div className="filter-group">
<label>Price:</label>
<InputNumber
placeholder="Min price"
value={filterPriceMin}
onChange={(value) => setFilterPriceMin(value)}
/>
<span> - </span>
<InputNumber
placeholder="Max price"
value={filterPriceMax}
onChange={(value) => setFilterPriceMax(value)}
/>
</div>
<div className="filter-group">
<label>Purchase Date:</label>
<RangePicker
format="YYYY-MM-DD"
onChange={(dates) => setFilterDateRange(dates)}
value={filterDateRange}
/>
</div>
</div>
{/* Button to open create item modal */} {/* Button to open create item modal */}
<button onClick={() => setIsCreateItemModalOpen(true)}> <button onClick={() => setIsCreateItemModalOpen(true)}>

View File

@@ -25,11 +25,10 @@ function getItem(key, label) {
const { SubMenu } = Menu; const { SubMenu } = Menu;
// Component
const Navbar = () => { const Navbar = () => {
//Hook calls
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
useEffect(() => { useEffect(() => {
console.log("NAVBAR EFFECT"); console.log("NAVBAR EFFECT");
isLoggedIn().then((user) => { isLoggedIn().then((user) => {
@@ -56,11 +55,10 @@ const Navbar = () => {
</Menu.Item>, </Menu.Item>,
]; ];
// Rendu du composant Navbar
try { try {
return ( return (
<Menu <Menu
defaultSelectedKeys={["1"]} defaultSelectedKeys={["0"]}
defaultOpenKeys={["sub1"]} defaultOpenKeys={["sub1"]}
theme="dark" theme="dark"
mode="inline" mode="inline"

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
// Composant Caract<63>ristique
export const Characteristic = ({ label, value }) => { export const Characteristic = ({ label, value }) => {
return ( return (
<div className="characteristic"> <div className="characteristic">

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
// Composant Description
export const Description = ({ title, children }) => { export const Description = ({ title, children }) => {
return ( return (
<div className="description"> <div className="description">

View File

@@ -2,18 +2,17 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { searchAndResizeImage } from '../../api/image-request' import { searchAndResizeImage } from '../../api/image-request'
// Composant Image export const Image = ({ src, alt, request }) => {
export const Image = ({ src, alt, request, _id }) => {
const [cacheUrl, setCacheUrl] = useState(null); const [cacheUrl, setCacheUrl] = useState(null);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
let cachedUrl = localStorage.getItem(_id); let cachedUrl = localStorage.getItem(request);
if (!cachedUrl) { if (!cachedUrl) {
try { try {
cachedUrl = await searchAndResizeImage(request); cachedUrl = await searchAndResizeImage(request);
localStorage.setItem(_id, cachedUrl); localStorage.setItem(request, cachedUrl);
console.log("Mise en cache de l'image avec l'ID : " + _id); console.log("Mise en cache de l'image avec la requ<71>te : " + request);
} catch (error) { } catch (error) {
console.error("Erreur lors de la r<>cup<75>ration de l'image : ", error); console.error("Erreur lors de la r<>cup<75>ration de l'image : ", error);
} }
@@ -22,13 +21,13 @@ export const Image = ({ src, alt, request, _id }) => {
}; };
fetchData(); fetchData();
}, [request, _id]); }, [request]);
if (src) { if (src) {
return <img src={src} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />; return <img src={src} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
} else if (cacheUrl) { } else if (cacheUrl) {
return <img src={cacheUrl} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />; return <img src={cacheUrl} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
} else { } else {//default image
return <img src={"https://media.discordapp.net/attachments/1164176196930637956/1167746303820840990/IMG_20231028_104620.jpg?ex=663ddefe&is=663c8d7e&hm=0985ce123fd1751f65388f7fefde5db6ce817e514e30f1d3c81eb28b15e78453&=&"} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />; return <img src={"https://cdn.discordapp.com/attachments/1164176196930637956/1213857681362784266/dinorundiscord.gif?ex=6642258f&is=6640d40f&hm=cf0ca54df3e002a15049618a6654b22c5d0c7943dc420e84936635725aceb90f&"} alt={alt} style={{ display: 'block', margin: 'auto' }} width='150px' height='150px' />;
} }
} }

View File

@@ -1,30 +1,23 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { FormCreateRoom } from "../../components/form/formCreateRoom"; import { FormCreateRoom } from "../../components/form/formCreateRoom";
//import { RoomUpdateForm } from './RoomUpdateForm'; import { FormUpdateRoom } from "../../components/form/formUpdateRoom";
export const RoomDetail = ({ export const RoomDetail = ({
selectedRoom, selectedRoomParam,
onCreateFormSubmit, onCreateFormSubmit
onUpdateFormSubmit,
onBack,
}) => { }) => {
const [isUpdateFormVisible, setIsUpdateFormVisible] = useState(false); const [isUpdateFormVisible, setIsUpdateFormVisible] = useState(false);
const [selectedRoom, setSelectedRoom] = useState(false);
useEffect(() => { useEffect(() => {
if (selectedRoom) { if (selectedRoomParam) {
setSelectedRoom(selectedRoomParam)
setIsUpdateFormVisible(true); setIsUpdateFormVisible(true);
} }
}, [selectedRoom]); }, [selectedRoomParam]);
// Afficher le formulaire de mise <20> jour lorsqu'une chambre est s<>lectionn<6E>e
const handleRoomClick = () => {
setIsUpdateFormVisible(true);
};
// Afficher le formulaire de cr<63>ation lorsqu'on revient en arri<72>re
const handleBackClick = () => { const handleBackClick = () => {
setIsUpdateFormVisible(false); setIsUpdateFormVisible(false);
onBack(null);
}; };
return ( return (
@@ -32,8 +25,8 @@ export const RoomDetail = ({
{isUpdateFormVisible ? ( {isUpdateFormVisible ? (
<div> <div>
<h2>Modifier une chambre</h2> <h2>Modifier une chambre</h2>
<button onClick={handleBackClick}>Annuler </button>
<FormCreateRoom onSubmit={onCreateFormSubmit} /> <FormUpdateRoom _id={selectedRoom} />
</div> </div>
) : ( ) : (
<div> <div>

View File

@@ -3,19 +3,10 @@ import "../../assets/styles/room-list.css";
import { RoomBox } from "../../components/rooms/roomBox"; import { RoomBox } from "../../components/rooms/roomBox";
import { formatRoomStats } from "../../api/room"; import { formatRoomStats } from "../../api/room";
// Fonction pour diviser le tableau d'items en rang<6E>es
const chunkArray = (arr, size) => {
const chunkedArr = [];
for (let i = 0; i < arr.length; i += size) {
chunkedArr.push(arr.slice(i, i + size));
}
return chunkedArr;
};
// Composant d'affichage de la page
export const RoomList = ({ statsParam, onRoomClick }) => { export const RoomList = ({ statsParam, onRoomClick }) => {
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
const [selectedRoom, setSelectedRoom] = useState("all");
useEffect(() => { useEffect(() => {
if (statsParam.rooms) { if (statsParam.rooms) {

View File

@@ -21,7 +21,7 @@ const RoomStats = ({statsParam}) => {
useEffect(() => { useEffect(() => {
if (stats) { if (stats) {
let table = Object.entries(stats.years) let table = Object.entries(stats.years)
.filter(([year, value]) => value !== 0) // Filtrer les entr<74>es o<> Y n'est pas <20>gal <20> 0 .filter(([year, value]) => value !== 0)
.map(([year, value]) => ({ name: year, value: value })); .map(([year, value]) => ({ name: year, value: value }));
setDiagramValues(table); setDiagramValues(table);
} }
@@ -55,7 +55,7 @@ const RoomStats = ({statsParam}) => {
<p>La chambre la plus ch<EFBFBD>re :</p> <p>La chambre la plus ch<EFBFBD>re :</p>
<ul> <ul>
<li><span>{stats?.global?.most_expensive_room?.name}</span></li> <li><span>{stats?.global?.most_expensive_room?.name}</span></li>
<li>Prix total : <span>{stats?.global?.most_expensive_room?.price}</span></li> <li>Prix total : <span>{stats?.global?.most_expensive_room?.count}</span></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -8,6 +8,7 @@ export const RoomBox = ({ room, onRoomClick }) => {
onRoomClick(room._id); onRoomClick(room._id);
}; };
return ( return (
<div className="room-details" onClick={handleBoxClick}> <div className="room-details" onClick={handleBoxClick}>
<Description title={room?.name}> <Description title={room?.name}>

View File

@@ -18,6 +18,10 @@ export const Login = () => {
} }
}; };
const onRegister = () => {
window.location.href = '/register'
}
return ( return (
<div> <div>
<h1>Login page</h1> <h1>Login page</h1>
@@ -36,6 +40,7 @@ export const Login = () => {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
<button type="submit">submit</button> <button type="submit">submit</button>
<button onClick={onRegister}>Register</button>
</form> </form>
</div> </div>
); );

View File

@@ -22,6 +22,7 @@ export const Register = () => {
return ( return (
<div> <div>
<h1>Register page</h1> <h1>Register page</h1>
<p>Password must include a capital letter, a digit and a symbol</p>
{error && <p className="text-red-500">{error}</p>} {error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<input <input

View File

@@ -44,8 +44,7 @@ export const Rooms = () => {
</div> </div>
<div className="detailContainer"> <div className="detailContainer">
<RoomDetail <RoomDetail
selectedRoom={selectedRoom} selectedRoomParam={selectedRoom}
onBack={handleRoomClick}
></RoomDetail> ></RoomDetail>
</div> </div>
</div> </div>