Merge remote-tracking branch 'origin/main' into Charles

This commit is contained in:
Charles Mendiburu 2025-04-04 11:14:47 +02:00
commit 37236d9ce3
40 changed files with 2182 additions and 164 deletions

View File

@ -49,7 +49,7 @@
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-pool</artifactId>
<version>1.16</version> <!-- Utilisez la version la plus récente -->
<version>1.16</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>

View File

@ -1,15 +1,14 @@
package com.example.starter;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.Tuple;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import at.favre.lib.crypto.bcrypt.BCrypt;
@ -45,6 +44,12 @@ public class MainVerticle extends AbstractVerticle {
router.route().handler(BodyHandler.create());
// Gestion des CORS
QueryObjects queryObjects = new QueryObjects(databaseService);
QueryWeatherData queryWeather = new QueryWeatherData(databaseService);
SetObjects setObjects = new SetObjects(databaseService);
// Create a Router
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(CorsHandler.create()
.addOrigin("*")
.allowedMethod(HttpMethod.GET)
@ -59,6 +64,12 @@ public class MainVerticle extends AbstractVerticle {
router.post("/login").handler(this::handleLogin); // Route pour la connexion
// Protéger toutes les routes commençant par "/api/"
router.route("/api/*").handler(JWTAuthHandler.create(jwtAuth));
router.get("/objets").handler(queryObjects::getObjects);
router.get("/objet").handler(queryObjects::getParticularObject);
router.post("/modifObjet").handler(setObjects::setInfoObjet);
router.get("/wind").handler(queryWeather::getWindInfos);
router.get("/meteo").handler(queryWeather::getMeteoInfos);
router.post("/addObject").handler(setObjects::newObject);
// Création du serveur HTTP
vertx.createHttpServer()
@ -258,4 +269,6 @@ public class MainVerticle extends AbstractVerticle {
}
}
}

View File

@ -1,5 +0,0 @@
package com.example.starter;
public class ProductHandler {
}

View File

@ -0,0 +1,86 @@
package com.example.starter;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.Tuple;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.time.format.DateTimeFormatter;
import io.vertx.ext.web.RoutingContext;
public class QueryObjects {
private DatabaseService databaseService;
public QueryObjects(DatabaseService dtbS){
this.databaseService = dtbS;
}
public void getObjects(RoutingContext context) {
databaseService.pool
.query("SELECT * FROM weather_objects;")
.execute()
.onFailure(e -> {
System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(getInfosObjects(rows).encode());
});
}
public void getParticularObject(RoutingContext context) {
String id = context.request().getParam("id");
if (id == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Paramètre 'id' manquant").encode());
return;
}
databaseService.pool
.preparedQuery("SELECT * FROM weather_objects WHERE id=?")
.execute(Tuple.of(Integer.parseInt(id)))
.onFailure(e -> {
System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
if (rows.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(getInfosObjects(rows).encode());
});
}
private JsonArray getInfosObjects(RowSet<Row> rows) {
JsonArray objects = new JsonArray();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss");
for (Row row : rows) {
JsonObject object = new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("description", row.getString("description"))
.put("type", row.getString("type"))
.put("location", row.getString("location"))
.put("last_update", row.getLocalDateTime("last_update").format(formatter))
.put("status", row.getString("status"))
.put("batterie",row.getInteger("batterie"))
.put("type_batterie",row.getString("type_batterie"));
objects.add(object);
}
return objects;
}
}

View File

@ -0,0 +1,99 @@
package com.example.starter;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import io.vertx.sqlclient.Tuple;
import java.time.format.DateTimeFormatter;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
public class QueryWeatherData {
private DatabaseService databaseService;
public QueryWeatherData(DatabaseService dtbS) {
this.databaseService = dtbS;
}
public void getWindInfos(RoutingContext context) {
String id = context.request().getParam("id");
if (id == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Paramètre 'id' manquant").encode());
return;
}
databaseService.pool
.preparedQuery("SELECT wind_speed,wind_direction,timestamp FROM weather_data WHERE station_id=?")
.execute(Tuple.of(Integer.parseInt(id)))
.onFailure(e -> {
System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
if (rows.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end((convertRowsToJson(rows)).encode());
});
}
public void getMeteoInfos(RoutingContext context) {
String id = context.request().getParam("id");
if (id == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Paramètre 'id' manquant").encode());
return;
}
databaseService.pool
.preparedQuery("SELECT temperature,humidity,pressure,timestamp FROM weather_data WHERE station_id=?")
.execute(Tuple.of(Integer.parseInt(id)))
.onFailure(e -> {
System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
if (rows.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end((convertRowsToJson(rows)).encode());
});
}
private JsonArray convertRowsToJson(RowSet<Row> rows) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss");
JsonArray jsonArray = new JsonArray();
for (Row row : rows) {
JsonObject jsonObject = new JsonObject();
for (int i = 0; i < row.size(); i++) {
String column = row.getColumnName(i);
if(column.compareTo("timestamp") == 0){
jsonObject.put("timestamp",row.getLocalDateTime("timestamp").format(formatter));
}else{
jsonObject.put(column, row.getValue(column));
}
}
jsonArray.add(jsonObject);
}
return jsonArray;
}
}

View File

@ -0,0 +1,89 @@
package com.example.starter;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Tuple;
public class SetObjects {
private DatabaseService databaseService;
public SetObjects(DatabaseService ddbs) {
this.databaseService = ddbs;
}
public void setInfoObjet(RoutingContext context) {
JsonObject body = context.body().asJsonObject();
if (body == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Corps de la requête manquant").encode());
return;
}
Integer id = body.getInteger("id");
String description = body.getString("description");
String type = body.getString("type");
String location = body.getString("location");
String status = body.getString("status");
databaseService.pool
.preparedQuery(
"UPDATE weather_objects SET description=?,type=?,location=?,status=?,last_update=CURRENT_TIMESTAMP WHERE id=?")
.execute(Tuple.of(description, type, location, status, id))
.onFailure(e -> {
System.err.println("Erreur de récupération de la BDD :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("Erreur", "Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
if (rows.rowCount() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "L'objet à bien été mis à jour").encode());
return;
});
}
public void newObject(RoutingContext context){
JsonObject body = context.body().asJsonObject();
if(body== null){
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error","Corps de la requête manquant").encode());
return;
}
String name = body.getString("nom");
String description = body.getString("description");
String type = body.getString("type");
String location = body.getString("location");
String status = body.getString("status");
databaseService.pool
.preparedQuery("INSERT INTO weather_objects (name,description,type,location,status) VALUES (?,?,?,?,?)")
.execute(Tuple.of(name,description,type,location,status))
.onFailure(e->{
System.err.println("Erreur de récupération de la BDD :"+e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error","Erreur de récupération de la BDD").encode());
})
.onSuccess(rows -> {
if(rows.rowCount()==0){
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
context.response()
.putHeader("content-type","application/json: charset=UTF-8")
.end(new JsonObject().put("success", "L'objet à bien été ajouté").encode());
return;
});
}
}

View File

@ -12,8 +12,11 @@
"axios": "^1.8.4",
"lucide-react": "^0.427.0",
"react": "^18.3.1",
"react-charts": "^3.0.0-beta.57",
"react-circle-progress-bar": "^0.1.4",
"react-dom": "^18.3.1",
"react-router-dom": "^7.4.0"
"react-router-dom": "^7.4.0",
"recharts": "^2.15.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
@ -262,6 +265,18 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
@ -1302,6 +1317,69 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -1314,6 +1392,44 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "17.0.85",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.85.tgz",
"integrity": "sha512-5oBDUsRDsrYq4DdyHaL99gE1AJCfuDhyxqF6/55fvvOIRkp1PpKuwJ+aMiGJR+GJt7YqMNclPROTHF20vY2cXA==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "^0.16",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz",
"integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -1656,6 +1772,15 @@
"node": ">= 6"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1742,6 +1867,127 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/d3-array": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
"license": "BSD-3-Clause",
"dependencies": {
"internmap": "^1.0.0"
}
},
"node_modules/d3-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
"license": "BSD-3-Clause"
},
"node_modules/d3-delaunay": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
"license": "ISC",
"dependencies": {
"delaunator": "4"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
"integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-interpolate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-color": "1 - 2"
}
},
"node_modules/d3-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
"integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==",
"license": "BSD-3-Clause"
},
"node_modules/d3-scale": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
"integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "^2.3.0",
"d3-format": "1 - 2",
"d3-interpolate": "1.2.0 - 2",
"d3-time": "^2.1.1",
"d3-time-format": "2 - 3"
}
},
"node_modules/d3-scale/node_modules/d3-time-format": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
"integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-time": "1 - 2"
}
},
"node_modules/d3-shape": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
"integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "1 - 2"
}
},
"node_modules/d3-time": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-array": "2"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -1759,12 +2005,24 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==",
"license": "ISC"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1786,6 +2044,16 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -2090,12 +2358,27 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@ -2497,6 +2780,12 @@
"node": ">=0.8.19"
}
},
"node_modules/internmap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
"license": "ISC"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -2707,6 +2996,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -2884,7 +3179,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -3197,6 +3491,23 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -3243,6 +3554,85 @@
"node": ">=0.10.0"
}
},
"node_modules/react-charts": {
"version": "3.0.0-beta.57",
"resolved": "https://registry.npmjs.org/react-charts/-/react-charts-3.0.0-beta.57.tgz",
"integrity": "sha512-vqas7IQhsnDGcMxreGaWXvSIL3poEMoUBNltJrslz/+m0pI3QejBCszL1QrLNYQfOWXrbZADfedi/a+yWOQ7Hw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.14.6",
"@types/d3-array": "^3.0.1",
"@types/d3-scale": "^4.0.1",
"@types/d3-shape": "^3.0.1",
"@types/raf": "^3.4.0",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"d3-array": "^2.12.1",
"d3-delaunay": "5.3.0",
"d3-scale": "^3.3.0",
"d3-shape": "^2.1.0",
"d3-time": "^2.1.1",
"d3-time-format": "^4.1.0",
"ts-toolbelt": "^9.6.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-circle-progress-bar": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/react-circle-progress-bar/-/react-circle-progress-bar-0.1.4.tgz",
"integrity": "sha512-2a47TDthNyUHJf8p1hv0wcTwIWnJBbEUfj/7dZcO+7BYd1W1sRC2t5x+SEWX9/1QT7hhf4t8ppcLaG0XrdwwgQ==",
"license": "MIT",
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
},
"node_modules/react-circle-progress-bar/node_modules/react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-circle-progress-bar/node_modules/react-dom": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.19.1"
},
"peerDependencies": {
"react": "^16.14.0"
}
},
"node_modules/react-circle-progress-bar/node_modules/scheduler": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -3255,6 +3645,12 @@
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -3304,6 +3700,37 @@
"react-dom": ">=18"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
"license": "MIT",
"dependencies": {
"fast-equals": "^5.0.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -3325,6 +3752,44 @@
"node": ">=8.10.0"
}
},
"node_modules/recharts": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
"integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
"react-is": "^18.3.1",
"react-smooth": "^4.0.4",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
"victory-vendor": "^36.6.8"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts-scale": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
"license": "MIT",
"dependencies": {
"decimal.js-light": "^2.4.1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -3709,6 +4174,12 @@
"node": ">=0.8"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -3727,6 +4198,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/ts-toolbelt": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
"license": "Apache-2.0"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
@ -3790,6 +4267,101 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/victory-vendor/node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/victory-vendor/node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/victory-vendor/node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/victory-vendor/node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/victory-vendor/node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/victory-vendor/node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/vite": {
"version": "5.4.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",

View File

@ -13,8 +13,11 @@
"axios": "^1.8.4",
"lucide-react": "^0.427.0",
"react": "^18.3.1",
"react-charts": "^3.0.0-beta.57",
"react-circle-progress-bar": "^0.1.4",
"react-dom": "^18.3.1",
"react-router-dom": "^7.4.0"
"react-router-dom": "^7.4.0",
"recharts": "^2.15.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",

View File

@ -6,6 +6,7 @@ import Gestion from "./pages/Gestion/Gestion.jsx";
import Header from "./components/Header.jsx";
import ObjectManagement from "./pages/Gestion/ObjectManagement.jsx";
import Objet from "./pages/Gestion/Objet.jsx";
import AddObject from "./pages/Gestion/AddObject.jsx";
import Signup from './pages/Signup.jsx';
import Login from './pages/Login.jsx';
@ -23,6 +24,7 @@ function App() {
<Route path="/objet" element={<Objet />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/ajouterObjet" element={<AddObject />} />
</Routes>
</div>
</Router>

View File

@ -0,0 +1,22 @@
import React, {useState} from "react";
import { TriangleAlert,X } from "lucide-react";
function AlertInactive({affAlert,setAffAlert}) {
return (
(affAlert&&(
<div className="flex flex-col md:flex-row bg-slate-600 w-full md:w-1/2 lg:w-1/3 fixed top-20 right-4 rounded-lg p-4 md:p-5 items-center gap-4 md:gap-6 shadow-lg opacity-90">
<button onClick={()=>setAffAlert(false)}className="absolute top-2 right-2 text-white hover:text-gray-300">
<X/>
</button>
<TriangleAlert className="text-red-700 w-12 h-12 md:w-16 md:h-16" />
<p className="text-sm md:text-base text-white text-center md:text-left">
Cet objet peut être inactif à son manque de données. Vous pouvez le
rendre inactif en appuyant <a>ici</a>.
</p>
</div>
)));
}
export default AlertInactive;

View File

@ -0,0 +1,27 @@
import React from "react";
import { Battery } from "lucide-react";
import Progress from "react-circle-progress-bar";
function BatterieInfo({ object }) {
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Battery className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1 ">
Etat de la batterie
</h1>
</div>
<div className="flex flex-col items-center">
<Progress progress={object.batterie} />
<h1 className="font-bold">
Type de batterie :{" "}
<span className="capitalize font-normal">{object.type_batterie}</span>
</h1>
</div>
</div>
);
}
export default BatterieInfo;

View File

@ -0,0 +1,27 @@
import React, {useRef} from "react";
import { ChartLine } from "lucide-react";
function BoutonGraphique({ TypeAff, setAffichage,graphCible}) {
const handleClick = (newAffichage) =>{
setAffichage(newAffichage);
if(graphCible.current){
graphCible.current.scrollIntoView({ behavior: "smooth" });
}
};
return !TypeAff ? (
<button
className="bg-blue-200 py-2 my-2 px-4 rounded-full mr-2"
onClick={() => handleClick(true)}
>
<ChartLine className="text-indigo-600" size={24} />
</button>
) : (
<button
className="bg-blue-400 py-2 my-2 px-4 rounded-full mr-2"
onClick={() => handleClick(false)}
>
<ChartLine className="text-indigo-600" size={24} />
</button>
);
}
export default BoutonGraphique;

View File

@ -0,0 +1,46 @@
import React from "react";
import { Info } from "lucide-react";
function InfoObject({ object,defafficherModif }) {
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-1">
<Info className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1 ">Informations</h1>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">Description :</p>
<p className="text-gray-600 capitalize">{object.description}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">Type :</p>
<p className="text-gray-600 capitalize">{object.type}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">Localisation :</p>
<p className="text-gray-600">{object.location}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">Status :</p>
<p className="text-gray-600 capitalize">{object.status}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">
Derniere mise à jour :
</p>
<p className="text-gray-600">{object.last_update}</p>
</div>
<div className="flex items-center gap-4 mb-1">
<a className="text-blue-500 hover:cursor-pointer" onClick={(()=>defafficherModif(true))}>Modifier ces infos</a>
</div>
</div>
);
}
export default InfoObject;

View File

@ -0,0 +1,90 @@
import React, { useEffect, useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from "recharts";
import { Wind } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
function MeteoGraph({ object, categorie, Logo }) {
const [rawData, setRawData] = useState([]);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/meteo?id=${identifiant}`).then((response) => {
setRawData(response.data);
});
}, [object]);
function getAvg() {
let moyenne = 0;
rawData.forEach((element) => {
if(element){
moyenne += element[categorie];
}
});
return moyenne / rawData.length;
}
return (
<div
key={object.id}
className="bg-white mb-6 p-6 rounded-xl min-w-5xl"
style={{ width: "100%", height: "400px" }}
>
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Logo className="text-indigo-600" size={24} />
</div>
{categorie === "temperature" ? (
<h1 className="text-black text-2xl font-bold mb-1 ">
Historique de la température
</h1>
) : categorie === "humidity" ? (
<h1 className="text-black text-2xl font-bold mb-1 ">
Historique de l'humidité
</h1>
) : (
<h1 className="text-black text-2xl font-bold mb-1 ">
Historique de la pression
</h1>
)}
</div>
<ResponsiveContainer width="100%" height="100%">
<LineChart
width={500}
height={300}
data={rawData}
margin={{
top: 5,
right: 30,
left: 10,
bottom: 60,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey={categorie}
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<ReferenceLine y={getAvg()} label="Moyenne" stroke="red" />
</LineChart>
</ResponsiveContainer>
</div>
);
}
export default MeteoGraph;

View File

@ -0,0 +1,130 @@
import { Thermometer, Sun, CircleGauge, Droplet } from "lucide-react";
import React, { useEffect, useState } from "react";
import axios from "axios";
import { API_BASE_URL } from "../config";
import BoutonGraphique from "./BoutonGraphique";
import AlertInactive from "./AlertInactive";
function MeteoInfos({
object,
defAffTempGraph,
AffTempGraph,
defAffPressionGraph,
AffPressionGraph,
defAffHumiditeGraph,
AffHumiditeGraph,
graphCible
}) {
const [rawData, setRawData] = useState([]);
const [AffAlert,setAffAlert] = useState(false);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/meteo?id=${identifiant}`).then((response) => {
setRawData(response.data);
if(rawData.length <5){
setAffAlert(true);
}
});
}, [object]);
const lastData = rawData.length > 0 ? rawData[rawData.length - 1] : null;
console.log(rawData.length);
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
{(AffAlert&&(object.status==="active")) && (
<AlertInactive affAlert={AffAlert} setAffAlert={setAffAlert} />
)}
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Sun className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1 ">Météo actuelle</h1>
</div>
{lastData ? (
<div className="flex flex-col items-center gap-4">
{lastData.temperature && (
<div className="bg-indigo-50 rounded-lg flex items-center w-full">
<div className="flex align-items gap-3 w-full justify-between">
<div className="flex align-items ml-3 gap-2">
<div className="flex items-center">
<Thermometer className="text-indigo-600" size={40} />
</div>
<div className="flex flex-col items-start">
<h1 className="text-indigo-600 text-xl font-bold ">
Température
</h1>
<h1 className="text-gray-700 text-4xl font-bold">
{lastData.temperature} <span className="text-lg">°C</span>
</h1>
</div>
</div>
<BoutonGraphique
TypeAff={AffTempGraph}
setAffichage={defAffTempGraph}
graphCible={graphCible}
/>
</div>
</div>
)}
{lastData.pressure && (
<div className="bg-indigo-50 rounded-lg flex items-center w-full">
<div className="flex align-items gap-3 w-full justify-between">
<div className="flex align-items ml-3 gap-2">
<div className="flex items-center">
<CircleGauge className="text-indigo-600" size={40} />
</div>
<div className="flex flex-col items-start">
<h1 className="text-indigo-600 text-xl font-bold ">
Pression
</h1>
<h1 className="text-gray-700 text-4xl font-bold">
{lastData.pressure} <span className="text-lg">hPa</span>
</h1>
</div>
</div>
<BoutonGraphique
TypeAff={AffPressionGraph}
setAffichage={defAffPressionGraph}
graphCible={graphCible}
/>
</div>
</div>
)}
{lastData.humidity && (
<div className="bg-indigo-50 rounded-lg flex items-center w-full">
<div className="flex align-items gap-3 w-full justify-between">
<div className="flex align-items ml-3 gap-2">
<div className="flex items-center">
<Droplet className="text-indigo-600" size={40} />
</div>
<div className="flex flex-col items-start">
<h1 className="text-indigo-600 text-xl font-bold ">
Humidité
</h1>
<h1 className="text-gray-700 text-4xl font-bold">
{lastData.humidity} <span className="text-lg">%</span>
</h1>
</div>
</div>
<BoutonGraphique
TypeAff={AffHumiditeGraph}
setAffichage={defAffHumiditeGraph}
/>
</div>
</div>
)}
<h1 className="text-gray-500 text-sm">
Dernier enregistrement : {lastData.timestamp}
</h1>
</div>
) : (
<p>Chargement des données...</p>
)}
</div>
);
}
export default MeteoInfos;

View File

@ -0,0 +1,158 @@
import React, { useState } from "react";
import { Info } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
function ModifObject({ object, defafficherModif }) {
const [description, setDescription] = useState(object.description || "");
const [type, setType] = useState(object.type || "");
const [location, setLocalisation] = useState(object.location || "");
const [status, setStatus] = useState(object.status || "inactive");
const [isActive, setActive] = useState(object.status === "active");
function handleSubmit(event) {
event.preventDefault(); // Empêche le rechargement de la page
axios
.post(`${API_BASE_URL}/modifObjet`, {
id: object.id,
description,
type,
location,
status,
})
.then((response) => {
console.log("Modification réussie :", response.data);
})
.catch((error) => {
console.error("Erreur lors de la modification :", error);
});
defafficherModif(false);
window.location.reload();
}
function handleCancel() {
defafficherModif(false);
}
function handleStatusChange() {
setActive((prevIsActive) => {
const newIsActive = !prevIsActive;
setStatus(newIsActive ? "active" : "inactive");
return newIsActive;
});
}
return (
<form onSubmit={handleSubmit} className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-9">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Info className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1">
Modifier les infos
</h1>
</div>
<div className="mb-5">
<label
htmlFor="description"
className="block mb-2 text-sm font-medium text-gray-900"
>
Description
</label>
<input
id="description"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
</div>
<div className="mb-5">
<label
htmlFor="type"
className="block mb-2 text-sm font-medium text-gray-900"
>
Type :
</label>
<input
id="type"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={type}
onChange={(e) => setType(e.target.value)}
required
/>
</div>
<div className="mb-5">
<label
htmlFor="location"
className="block mb-2 text-sm font-medium text-gray-900"
>
Localisation :
</label>
<input
id="location"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={location}
onChange={(e) => setLocalisation(e.target.value)}
required
/>
</div>
<div className="mb-5">
<label className="block mb-2 text-sm font-medium text-gray-900">
Status :
</label>
<div className="inline-flex items-center gap-2">
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
Inactive
</label>
<div className="relative inline-block w-11 h-5">
<input
id="switch-component-on"
type="checkbox"
checked={isActive}
onChange={handleStatusChange}
className="peer appearance-none w-11 h-5 bg-slate-100 rounded-full checked:bg-slate-800 cursor-pointer transition-colors duration-300"
/>
<label
htmlFor="switch-component-on"
className="absolute top-0 left-0 w-5 h-5 bg-white rounded-full border border-slate-300 shadow-sm transition-transform duration-300 peer-checked:translate-x-6 peer-checked:border-slate-800 cursor-pointer"
></label>
</div>
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
Active
</label>
</div>
</div>
<div className="mb-5 flex flex-col">
<button
type="submit"
className="text-blue-500 hover:cursor-pointer hover:underline"
>
Confirmer les modifications
</button>
<button
type="button"
className="text-red-500 hover:cursor-pointer hover:underline"
onClick={handleCancel}
>
Annuler les modifications
</button>
</div>
</form>
);
}
export default ModifObject;

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState} from "react";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Wind } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
function WindGraph({ object }) {
const [rawData, setRawData] = useState([]);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/wind?id=${identifiant}`).then((response) => {
setRawData(response.data);
});
}, [object]);
const CustomTooltip = ({ payload, label, active }) => {
if (active && payload && payload.length) {
const { wind_speed, timestamp,wind_direction } = payload[0].payload;
return (
<div className="custom-tooltip">
<p><strong>Date:</strong> {timestamp}</p>
<p><strong>Vitesse du vent:</strong> {wind_speed} km/h</p>
<p><strong>Direction du vent:</strong> {wind_direction}</p>
</div>
);
}
return null; // Si aucun point n'est survolé
};
return (
<div key={object.id} className="bg-white mb-6 p-6 rounded-xl min-w-5xl" style={{width: "100%", height: "400px"}}>
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Wind className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1 ">Historique du vent</h1>
</div>
<ResponsiveContainer width="100%" height="100%">
<LineChart
width={500}
height={300}
data={rawData}
margin={{
top: 5,
right: 30,
left: 10,
bottom: 60,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip content={<CustomTooltip/>}/>
<Legend />
<Line type="monotone" dataKey="wind_speed" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>
</div>
);
}
export default WindGraph;

View File

@ -0,0 +1,67 @@
import { Wind } from "lucide-react";
import React, { useEffect, useState } from "react";
import axios from "axios";
import { API_BASE_URL } from "../config";
import BoutonGraphique from "./BoutonGraphique";
function WindInfo({ object, defAffWindGraph, AffWindGraph }) {
const [rawData, setRawData] = useState([]);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/wind?id=${identifiant}`).then((response) => {
setRawData(response.data);
});
}, [object]);
const lastData = rawData.length > 0 ? rawData[rawData.length - 1] : null;
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-1">
<Wind className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold">Vent actuel</h1>
</div>
{lastData ? (
<div className="flex flex-col items-center gap-1">
<img
src={`./src/img/${lastData.wind_direction}.png`}
alt="Wind Direction"
className="h-25"
/>
<h1 className="text-gray-600 text-xl font-bold mb-1 ">
{lastData.wind_direction}
</h1>
<div className="bg-indigo-50 rounded-lg flex flex-col items-center mb-1 w-full">
<div className="flex align-items gap-3 w-full justify-between">
<div className="flex align-items ml-3 gap-2">
<div className="flex items-center">
<Wind className="text-indigo-600" size={40} />
</div>
<div className="flex flex-col items-start">
<h1 className="text-indigo-600 text-xl font-bold ">Valeur</h1>
<h1 className="text-gray-700 text-4xl font-bold">
{lastData.wind_speed} <span className="text-lg">Km/h</span>
</h1>
</div>
</div>
<BoutonGraphique
TypeAff={AffWindGraph}
setAffichage={defAffWindGraph}
/>
</div>
</div>
<h1 className="text-gray-500 text-sm">
Dernier enregistrement : {lastData.timestamp}
</h1>
</div>
) : (
<p>Chargement des données...</p>
)}
</div>
);
}
export default WindInfo;

1
Front-end/src/config.js Normal file
View File

@ -0,0 +1 @@
export const API_BASE_URL = 'http://localhost:8888';

BIN
Front-end/src/img/Est.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
Front-end/src/img/Nord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
Front-end/src/img/Ouest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
Front-end/src/img/Sud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,9 +1,166 @@
import React from 'react';
import React from "react";
function About() {
return (
<div>
<h1>A propos</h1>
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-5">
{/* Grille principale */}
<div className="grid md:grid-cols-2 gap-10 lg:gap-20 mb-5">
{/* Section Notre mission */}
<div className="order-1 md:order-1">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Notre mission
</h1>
<p className="text-gray-700 leading-relaxed">
Notre mission est de fournir une solution complète et innovante
pour la surveillance climatique et environnementale du
territoire français. En combinant des prévisions météorologiques
de haute qualité avec une gestion efficace des objets connectés,
nous visons à offrir une plateforme centralisée permettant de
surveiller en temps réel les conditions météorologiques locales,
tout en facilitant l'analyse des données collectées par des
objets connectés déployés à travers le pays.
</p>
</div>
<img
className="rounded-lg h-64 w-full object-cover order-2 md:order-2"
src="./src/img/NotreMission.png"
alt="Notre mission"
/>
{/* Section Qui sommes-nous */}
<img
className="rounded-lg h-64 w-full object-cover order-4 md:order-3"
src="./src/img/iotmeteo.jpg"
alt="IoT et météo"
/>
<div className="order-3 md:order-4">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Qui sommes-nous ?
</h1>
<p className="text-gray-700 leading-relaxed">
Nous sommes une équipe de passionnés de technologie,
dinnovation et denvironnement. Nous croyons fermement que la
combinaison de la donnée météorologique en temps réel et de
lInternet des Objets (IoT) peut avoir un impact majeur sur la
gestion des territoires. Que ce soit pour les collectivités
locales, les entreprises ou les acteurs publics, notre
plateforme offre les outils nécessaires pour une gestion
proactive et réactive de lenvironnement.
</p>
</div>
{/* Section Notre Vision */}
<div className="order-5 md:order-5">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Notre Vision
</h1>
<p className="text-gray-700 leading-relaxed">
Dans un monde les conditions climatiques évoluent rapidement,
il est essentiel de pouvoir anticiper et réagir efficacement
face aux phénomènes météorologiques. Grâce à nos objets
connectés et à notre interface intuitive, nous permettons aux
utilisateurs de suivre les conditions en temps réel et dagir en
conséquence. De la gestion des risques climatiques à la
planification urbaine, notre plateforme aide les décideurs à
prendre des décisions éclairées basées sur des données fiables
et locales.
</p>
</div>
<img
className="rounded-lg h-64 w-full object-cover order-6 md:order-6"
src="./src/img/surveillancemeteo.webp"
alt="Surveillance météo"
/>
</div>
{/* Section Objectifs */}
<div className="text-center col-span-2 order-7">
<h1 className="text-2xl font-bold text-gray-900 mb-10 mt-20">
Les Objectifs de Notre Plateforme
</h1>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-10">
{/* Objectif 1 */}
<div className="relative group w-full h-80 mb-7">
<img
src="./src/img/surveillancetempsreel.jpg"
alt="Surveillance en temps réel"
className="w-full h-full object-cover rounded-xl"
/>
<div className="absolute inset-0 bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl flex items-center justify-center">
<p className="text-white text-lg font-bold px-4">
Grâce à nos objets connectés, nous collectons des données
météorologiques locales, permettant une surveillance
continue des conditions climatiques sur tout le territoire
français.
</p>
</div>
<h1 className="text-xl font-bold mt-4 ">
Surveillance en temps réel
</h1>
</div>
{/* Objectif 2 */}
<div className="relative group w-full h-80 mb-7">
<img
src="./src/img/precisionfiable.jpg"
alt="Précision fiable"
className="w-full h-full object-cover rounded-xl"
/>
<div className="absolute inset-0 bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl flex items-center justify-center">
<p className="text-white text-lg font-bold px-4">
En utilisant les meilleures technologies de prévision
météorologique, nous vous fournissons des prévisions
précises, quil sagisse de la température, de la vitesse du
vent ou de la qualité de lair.
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">Prédiction fiable</h1>
</div>
{/* Objectif 3 */}
<div className="relative group w-full h-80 mb-7 border-2 rounded-xl">
<img
src="./src/img/gestioniot.png"
alt="Gestion IoT"
className="w-full h-full object-cover rounded-xl"
/>
<div className="absolute inset-0 bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl flex items-center justify-center">
<p className="text-white text-lg font-bold px-4">
Nous permettons aux utilisateurs de gérer facilement leurs
objets connectés (stations météo, capteurs, etc.) à travers
une interface simple, tout en offrant un suivi en temps réel
de leur statut et de leurs données.
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">
Gestion des objets connectés
</h1>
</div>
{/* Objectif 4 */}
<div className="relative group w-full h-80 mb-7">
<img
src="./src/img/fr-alert.webp"
alt="Réponse rapide"
className="w-full h-full object-cover rounded-xl"
/>
<div className="absolute inset-0 bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-xl flex items-center justify-center">
<p className="text-white text-lg font-bold px-4">
Notre plateforme vous envoie des alertes instantanées
concernant les phénomènes météorologiques extrêmes, vous
permettant de prendre des décisions rapides et adaptées.
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">
Réponse rapide aux alertes climatiques
</h1>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,220 @@
import React, { useState } from "react";
import { BadgePlus } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../../config";
function AddObject() {
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [location, setLocalisation] = useState("");
const [status, setStatus] = useState("active");
const [nom, setNom] = useState("");
const [Response, setResponse] = useState(null);
const [isActive, setActive] = useState(true);
const [verif, setVerif] = useState(false);
const [enregistre, setEnregistre] = useState(false);
const [messRequete, setMessRequete] = useState("");
function handleSubmit(event) {
event.preventDefault();
if (verif) {
console.log("Envoi requete");
axios
.post(`${API_BASE_URL}/addObject`, {
nom,
description,
type,
location,
status,
})
.then((response) => {
setMessRequete("Votre objet à bien été enregistré !");
setEnregistre(true);
console.log("Ajout de l'objet réussit :", response.data);
})
.catch((error) => {
setMessRequete("Il y a eu une erreur dans l'ajout de votre objet !");
console.error("Erreur lors de l'ajout de l'objet :", error);
});
setVerif(false);
resetForm();
} else {
setVerif(true);
}
}
function resetForm() {
setNom("");
setStatus("");
setDescription("");
setType("");
setLocalisation("");
setActive(true);
}
function handleCancel() {
if (verif) {
setVerif(false);
} else {
resetForm();
}
}
function handleStatusChange() {
setActive((prevIsActive) => {
const newIsActive = !prevIsActive;
setStatus(newIsActive ? "active" : "inactive");
return newIsActive;
});
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
<div className=" max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-5">
<h2 className="text-4xl font-bold text-gray-900 mb-12">
Nouvel objet
</h2>
</div>
<form
onSubmit={handleSubmit}
className="bg-white p-6 rounded-xl min-w-5xl"
>
<div className="flex align-items gap-9">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<BadgePlus className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1">
{!verif
? "Entrez les données de votre nouvel objet"
: "Êtes-vous sûr de ces données ?"}
</h1>
</div>
<div className="mb-5">
<label
htmlFor="nom"
className="block mb-2 text-sm font-medium text-gray-900"
>
Nom :
</label>
<input
id="nom"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
required
disabled={verif}
/>
</div>
<div className="mb-5">
<label
htmlFor="description"
className="block mb-2 text-sm font-medium text-gray-900"
>
Description :
</label>
<input
id="description"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
disabled={verif}
/>
</div>
<div className="mb-5">
<label
htmlFor="type"
className="block mb-2 text-sm font-medium text-gray-900"
>
Type :
</label>
<input
id="type"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={type}
onChange={(e) => setType(e.target.value)}
required
disabled={verif}
/>
</div>
<div className="mb-5">
<label
htmlFor="location"
className="block mb-2 text-sm font-medium text-gray-900"
>
Localisation :
</label>
<input
id="location"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={location}
onChange={(e) => setLocalisation(e.target.value)}
required
disabled={verif}
/>
</div>
<div className="mb-5">
<label className="block mb-2 text-sm font-medium text-gray-900">
Status :
</label>
<div className="inline-flex items-center gap-2">
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
Inactive
</label>
<div className="relative inline-block w-11 h-5">
<input
id="switch-component-on"
type="checkbox"
checked={isActive}
onChange={handleStatusChange}
className="peer appearance-none w-11 h-5 bg-slate-100 rounded-full checked:bg-slate-800 cursor-pointer transition-colors duration-300"
disabled={verif}
/>
<label
htmlFor="switch-component-on"
className="absolute top-0 left-0 w-5 h-5 bg-white rounded-full border border-slate-300 shadow-sm transition-transform duration-300 peer-checked:translate-x-6 peer-checked:border-slate-800 cursor-pointer"
></label>
</div>
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
Active
</label>
</div>
</div>
<div className="flex flex-col mb-5 ">
<button
type={"submit"}
className="text-blue-500 hover:cursor-pointer hover:underline mb-2"
>
{!verif ? "Confirmer les informations" : "Oui je suis sûr !"}
</button>
<button
type="button"
className="text-red-500 hover:cursor-pointer hover:underline"
onClick={handleCancel}
>
{!verif ? "Supprimer les informations" : "Non je veux changer !"}
</button>
</div>
<p className={(enregistre)?("text-green-700"):("text-red-700")}>
{messRequete}
</p>
</form>
</div>
</div>
);
}
export default AddObject;

View File

@ -10,9 +10,8 @@ import {
RadioTower,
Binoculars,
Settings,
ChartArea,
BadgePlus,
} from "lucide-react";
function Gestion() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
@ -22,84 +21,43 @@ function Gestion() {
Bienvenue dans le module <b>Gestion</b>.
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Dans ce module, vous allez pouvoir gerer les objets connectés de
l'hopital.
Ce module vous permet de gérer les capteur et stations connectés de France de manière simple et efficace.
</p>
</div>
{/* Features Grid */}
<div className="grid md:grid-cols-3 gap-8">
<div className="grid md:grid-cols-2 gap-8">
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<RadioTower className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
Gestion des objets connectés
Consulter les objets connectés météorologiques
</h3>
<p className="text-gray-600 mb-4">
Découvrez les meilleurs endroits de votre ville, qu'il s'agisse de
restaurants, de parcs ou de lieux culturels.
Accédez aux données en temps réel des objets connectés météorologiques, modifiez leurs paramètres et consultez l'historique des mesures.
</p>
<a
href="/gestionObjets"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
Explorer les lieux <ArrowRight size={16} className="ml-2" />
Explorer les objets <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Settings className="text-indigo-600" size={24} />
<BadgePlus className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
Configurer des services
Ajouter un nouvel objet connecté
</h3>
<p className="text-gray-600 mb-4">
Découvrez les meilleurs endroits de votre ville, qu'il s'agisse de
restaurants, de parcs ou de lieux culturels.
Intégrez facilement un nouvel objet connecté en renseignant ses informations et en configurant ses paramètres pour une gestion optimale.
</p>
<a
href="#"
href="/ajouterObjet"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
Explorer les lieux <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Binoculars className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
Surveillance et optimisation des ressources
</h3>
<p className="text-gray-600 mb-4">
Restez informé des derniers événements, festivals et
rassemblements communautaires dans votre région.
</p>
<a
href="#"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
Voir les événements <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<ChartArea className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
Rapports statistiques
</h3>
<p className="text-gray-600 mb-4">
Accédez en temps réel aux horaires et aux itinéraires des bus, des
trains et des autres transports publics.
</p>
<a
href="#"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
Vérifier les horaires <ArrowRight size={16} className="ml-2" />
Ajouter un objet <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>

View File

@ -1,29 +1,31 @@
import React from "react";
import {
Search,
MapPin,
Calendar,
Bus,
ArrowRight,
LogIn,
UserPlus,
RadioTower,
Binoculars,
Settings,
ChartArea,
} from "lucide-react";
import { Search, ArrowRight, RadioTower,Plus } from "lucide-react";
import { useEffect, useState } from "react";
import axios from "axios";
import { useFetcher } from "react-router-dom";
function ObjectManagement() {
const [searchQuery, setSearchQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("all");
const [activeFilter, setActiveFilter] = useState("");
const [objects, setObjects] = useState([]);
const [nbAffObject,setnbAffObject] = useState(6);
const filteredDATA = objects.filter((node) => {
const matchesSearchQuery =
searchQuery === "" ||
node.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
node.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesTag =
activeFilter === "" ||
node.name.toLowerCase().includes(activeFilter.toLowerCase()) ||
node.description.includes(activeFilter.toLowerCase()) ||
(activeFilter === "Active" && node.status.toLowerCase() === "active") ||
(activeFilter === "Inactive" && node.status.toLowerCase() === "inactive");
return matchesSearchQuery && matchesTag;
});
useEffect(() => {
axios.get("http://localhost:8888/objets").then((response) => {
setObjects(response.data);
console.log(response.data);
});
}, []);
return (
@ -50,9 +52,9 @@ function ObjectManagement() {
</div>
<div className="flex gap-4 mt-4 justify-center">
<button
onClick={() => setActiveFilter("all")}
onClick={() => setActiveFilter("")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "all"
activeFilter === ""
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
@ -60,19 +62,9 @@ function ObjectManagement() {
Tout
</button>
<button
onClick={() => setActiveFilter("locations")}
onClick={() => setActiveFilter("Station")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "locations"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
Villes
</button>
<button
onClick={() => setActiveFilter("events")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "events"
activeFilter === "Station"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
@ -80,29 +72,66 @@ function ObjectManagement() {
Station météo
</button>
<button
onClick={() => setActiveFilter("transport")}
onClick={() => setActiveFilter("Capteur")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "transport"
activeFilter === "Capteur"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
Capteur
</button>
<button
onClick={() => setActiveFilter("Active")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "Active"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
Actif
</button>
<button
onClick={() => setActiveFilter("Inactive")}
className={`px-4 py-2 rounded-lg ${
activeFilter === "Inactive"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
Inactif
</button>
</div>
</div>
<div className="grid md:grid-cols-3 gap-8">
{objects.map(object =>(
<div key={object.id} className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
{filteredDATA.length === 0 ? (
<p>Aucun objet trouvé</p>
) : (
filteredDATA.slice(0,nbAffObject).map((object) => (
<div
key={object.id}
className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow "
>
{object.status === "active" ? (
<div className="relative w-full">
<span className="absolute right-0 flex size-3">
<span className="absolute inline-flex h-full w-full rounded-full animate-ping bg-green-400 opacity-75"></span>
<span className="relative inline-flex size-3 rounded-full bg-green-500"></span>
</span>
</div>
) : (
<div className="relative w-full">
<span className="absolute right-0 flex size-3">
<span className="relative inline-flex size-3 rounded-full bg-red-600"></span>
</span>
</div>
)}
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<RadioTower className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
{object.name}
</h3>
<p className="text-gray-600 mb-4">
{object.description}
</p>
<h3 className="text-xl font-semibold mb-2">{object.name}</h3>
<p className="text-gray-600 mb-4">{object.description}</p>
<a
href={`/objet?id=${object.id}`}
className="flex items-center text-indigo-600 hover:text-indigo-700"
@ -110,8 +139,15 @@ function ObjectManagement() {
Plus d'infos <ArrowRight size={16} className="ml-2" />
</a>
</div>
))}
))
)}
</div>
{(nbAffObject<filteredDATA.length)&&(
<div className="flex items-center flex-col mt-6">
<button onClick={()=>{setnbAffObject((prev)=>prev+6 )}}><Plus size={40}/></button>
<label>Voir plus</label>
</div>
)}
</div>
</div>
);

View File

@ -1,58 +1,117 @@
import React from "react";
import {
Search,
MapPin,
Calendar,
Bus,
ArrowRight,
LogIn,
UserPlus,
RadioTower,
Binoculars,
Settings,
ChartArea,
} from "lucide-react";
import { useEffect, useState } from "react";
import axios from "axios";
import { useFetcher } from "react-router-dom";
import { Thermometer, CircleGauge, Droplet } from "lucide-react";
import { useEffect, useState, useRef} from "react";
import axios from "axios";
import { API_BASE_URL } from "../../config";
import InfoObjet from "../../components/InfoObject";
import ModifObject from "../../components/ModifObject";
import WindGraph from "../../components/WindGraph";
import WindInfo from "../../components/WindInfo";
import MeteoInfos from "../../components/MeteoInfos";
import MeteoGraph from "../../components/MeteoGraph";
import BatterieInfo from "../../components/BatterieInfo";
function Objet() {
const identifiant = new URLSearchParams(window.location.search).get("id");
const [searchQuery, setSearchQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("all");
const [object, setObject] = useState({});
const [graphStates, setGraphStates] = useState({
wind:false,
temperature:false,
pressure:false,
humidity:false,
})
const [afficherModif, defafficherModif] = useState(false);
const [AffWindGraph, defAffWindGraph] = useState(false);
const [AffTempGraph, defAffTempGraph] = useState(false);
const [AffPressionGraph, defAffPressionGraph] = useState(false);
const [AffHumiditeGraph, defAffHumideGraph] = useState(false);
const tempGraphRef = useRef(null);
const pressureGraphRef = useRef(null);
const humidityGraphRef = useRef(null);
const windGraphRef = useRef(null);
useEffect(() => {
axios.get(`http://localhost:8888/objet?id=${identifiant}`).then((response) => {
axios.get(`${API_BASE_URL}/objet?id=${identifiant}`).then((response) => {
setObject(response.data[0]);
console.log(response.data);
});
}, [identifiant]);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
<div className=" max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
{object.name}
<div className="text-center mb-5">
<h2 className="text-4xl font-bold text-gray-900 mb-12">
Tableau de bord - {object.name}
</h2>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div
key={object.id}
className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow"
>
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<RadioTower className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">{object.name}</h3>
<p className="text-gray-600 mb-4">{object.description}</p>
<a
href={`/objet?id=${object.id}`}
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
Plus d'infos <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="grid md:grid-cols-1 lg:grid-cols-3 gap-8 mb-5">
{!afficherModif ? (
<InfoObjet object={object} defafficherModif={defafficherModif} />
) : (
<ModifObject object={object} defafficherModif={defafficherModif} />
)}
{object && object.id ? (
<WindInfo
object={object}
defAffWindGraph={defAffWindGraph}
AffWindGraph={AffWindGraph}
/>
) : (
<p>Chargement des données...</p>
)}
{object && object.id ? (
<MeteoInfos
object={object}
defAffTempGraph={defAffTempGraph}
AffTempGraph={AffTempGraph}
defAffPressionGraph={defAffPressionGraph}
AffPressionGraph={AffPressionGraph}
defAffHumiditeGraph={defAffHumideGraph}
AffHumiditeGraph={AffHumiditeGraph}
tempGraphRef={tempGraphRef}
pressureGraphRef={pressureGraphRef}
humidityGraphRef={humidityGraphRef}
/>
) : (
<p>Chargement des données...</p>
)}
<BatterieInfo object={object} />
</div>
{AffWindGraph &&
(object && object.id ? (
<WindGraph object={object} />
) : (
<p>Chargement des données...</p>
))}
{AffTempGraph &&
(object && object.id ? (
<MeteoGraph
object={object}
categorie={"temperature"}
Logo={Thermometer}
/>
) : (
<p>Chargement des données...</p>
))}
{AffPressionGraph &&
(object && object.id ? (
<MeteoGraph
object={object}
categorie={"pressure"}
Logo={CircleGauge}
/>
) : (
<p>Chargement des données...</p>
))}
{AffHumiditeGraph &&
(object && object.id ? (
<MeteoGraph object={object} categorie={"humidity"} Logo={Droplet} />
) : (
<p>Chargement des données...</p>
))}
</div>
</div>
);

View File

@ -1,18 +1,117 @@
import React, { useState } from 'react';
function Home() {
const token = localStorage.getItem("token");
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState('all');
const [name, setName] = useState([]);
return (
<div className='max-w-[1296px] m-auto flex flex-col md:flex-row items-center justify-between p-[50px_0_60px] md:p-[104px_0]'>
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Bienvenue dans ta ville intelligente.</h2>
{token ? (
<>Home
<img src='public/images/snow.jpg' />
</>):(
<h2>Non connecté</h2>
)}
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Découvrez tout ce que votre ville a à offrir - des événements locaux aux horaires de transport, le tout en un seul endroit.
</p>
</div>
)
<div className="max-w-3xl mx-auto mb-12">
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400" size={24} />
<input
type="text"
placeholder="Search for locations, events, or transport..."
className="w-full pl-12 pr-4 py-4 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex gap-4 mt-4 justify-center">
<button
onClick={() => setActiveFilter('all')}
className={`px-4 py-2 rounded-lg ${
activeFilter === 'all' ? 'bg-indigo-600 text-white' : 'bg-white text-gray-600'
}`}
>
All
</button>
<button
onClick={() => setActiveFilter('locations')}
className={`px-4 py-2 rounded-lg ${
activeFilter === 'locations' ? 'bg-indigo-600 text-white' : 'bg-white text-gray-600'
}`}
>
Locations
</button>
<button
onClick={() => setActiveFilter('events')}
className={`px-4 py-2 rounded-lg ${
activeFilter === 'events' ? 'bg-indigo-600 text-white' : 'bg-white text-gray-600'
}`}
>
Events
</button>
<button
onClick={() => setActiveFilter('transport')}
className={`px-4 py-2 rounded-lg ${
activeFilter === 'transport' ? 'bg-indigo-600 text-white' : 'bg-white text-gray-600'
}`}
>
Transport
</button>
</div>
</div>
{/* Features Grid */}
<div className="grid md:grid-cols-3 gap-8">
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<MapPin className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">Points d'intérêt</h3>
<p className="text-gray-600 mb-4">
Découvrez les meilleurs endroits de votre ville, qu'il s'agisse de restaurants, de parcs ou de lieux culturels.
</p>
<a href="#" className="flex items-center text-indigo-600 hover:text-indigo-700">
Explorer les lieux <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Calendar className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">Evenements locaux</h3>
<p className="text-gray-600 mb-4">
Restez informé des derniers événements, festivals et rassemblements communautaires dans votre région.
</p>
<a href="#" className="flex items-center text-indigo-600 hover:text-indigo-700">
Voir les événements <ArrowRight size={16} className="ml-2" />
</a>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<Bus className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">Transports publics</h3>
<p className="text-gray-600 mb-4">
Accédez en temps réel aux horaires et aux itinéraires des bus, des trains et des autres transports publics.
</p>
<a href="#" className="flex items-center text-indigo-600 hover:text-indigo-700">
Vérifier les horaires <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
</div>
</div>
);
}
export default Home;