Add z1 directory and move of the project

This commit is contained in:
Charles Mendiburu 2026-03-31 20:24:20 +02:00
parent 1f9109d3dd
commit f5dec349d2
93 changed files with 9039 additions and 0 deletions

11
z1/Back-end/.editorconfig Normal file
View File

@ -0,0 +1,11 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true

185
z1/Back-end/.gitignore vendored Normal file
View File

@ -0,0 +1,185 @@
### Vert.x ###
.vertx/
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
### Intellij+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-buildTool-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-buildTool.properties
fabric.properties
### Intellij+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### macOS ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
### Gradle ###
.gradle
/buildTool/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
### NetBeans ###
nbproject/private/
buildTool/
nbbuild/
dist/
nbdist/
.nb-gradle/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

Binary file not shown.

View File

@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

25
z1/Back-end/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# Stage 1: Build the Maven artifact
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
# Optimize build by caching dependencies where possible
COPY pom.xml .
# Download dependencies
RUN mvn dependency:go-offline -B
# Copy source code and build the JAR
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: Setup the runtime environment
FROM eclipse-temurin:17-jre
WORKDIR /app
# Copy the constructed fat JAR from the builder stage
COPY --from=build /app/target/starter-1.0.0-SNAPSHOT-fat.jar ./app.jar
COPY keystore.jceks ./
EXPOSE 8888
# Start the Vert.x application
ENTRYPOINT ["java", "-jar", "app.jar"]

31
z1/Back-end/README.adoc Normal file
View File

@ -0,0 +1,31 @@
= Starter
image:https://img.shields.io/badge/vert.x-4.5.13-purple.svg[link="https://vertx.io"]
This application was generated using http://start.vertx.io
== Building
To launch your tests:
```
./mvnw clean test
```
To package your application:
```
./mvnw clean package
```
To run your application:
```
./mvnw clean compile exec:java
```
== Help
* https://vertx.io/docs/[Vert.x Documentation]
* https://stackoverflow.com/questions/tagged/vert.x?sort=newest&pageSize=15[Vert.x Stack Overflow]
* https://groups.google.com/forum/?fromgroups#!forum/vertx[Vert.x User Group]
* https://discord.gg/6ry7aqPWXy[Vert.x Discord]

BIN
z1/Back-end/keystore.jceks Normal file

Binary file not shown.

Binary file not shown.

308
z1/Back-end/mvnw vendored Executable file
View File

@ -0,0 +1,308 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

205
z1/Back-end/mvnw.cmd vendored Normal file
View File

@ -0,0 +1,205 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%

164
z1/Back-end/pom.xml Normal file
View File

@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-shade-plugin.version>3.2.4</maven-shade-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<exec-maven-plugin.version>3.0.0</exec-maven-plugin.version>
<vertx.version>4.5.13</vertx.version>
<junit-jupiter.version>5.9.1</junit-jupiter.version>
<main.verticle>com.example.starter.MainVerticle</main.verticle>
<launcher.class>io.vertx.core.Launcher</launcher.class>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>${vertx.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-api</artifactId>
<version>1.16</version>
</dependency>
<dependency>
<groupId>io.agroal</groupId>
<artifactId>agroal-pool</artifactId>
<version>1.16</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-jdbc-client</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency> <!--Dependy pour hacher le mdp-->
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-jwt</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>${maven-shade-plugin.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${launcher.class}</Main-Class>
<Main-Verticle>${main.verticle}</Main-Verticle>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar
</outputFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<configuration>
<mainClass>${launcher.class}</mainClass>
<arguments>
<argument>run</argument>
<argument>${main.verticle}</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,169 @@
package com.example.starter;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import at.favre.lib.crypto.bcrypt.BCrypt;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.Row;
public class AuthHandler {
private final DatabaseService databaseService;
private final JWTAuth jwtAuth;
public AuthHandler(DatabaseService databaseService, JWTAuth jwtAuth) {
this.databaseService = databaseService;
this.jwtAuth = jwtAuth;
}
public void handleSignup(RoutingContext context) {
JsonObject body = context.body().asJsonObject();
if (body == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Requête invalide").encode());
return;
}
String name = body.getString("name");
String surname = body.getString("surname");
String email = body.getString("email");
String gender = body.getString("gender");
String password = body.getString("password");
String pseudo = body.getString("pseudo");
if (name == null || surname == null || email == null || gender == null || password == null || pseudo == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Tous les champs sont requis").encode());
return;
}
String hashedPassword = BCrypt.withDefaults().hashToString(12, password.toCharArray());
databaseService.pool
.preparedQuery("INSERT INTO users (name, surname, email, gender, password, pseudo) VALUES (?, ?, ?, ?, ?, ?)")
.execute(Tuple.of(name, surname, email, gender, hashedPassword, pseudo))
.onSuccess(result -> {
context.response()
.setStatusCode(201)
.end(new JsonObject().put("message", "Utilisateur inscrit avec succès").encode());
})
.onFailure(err -> {
System.err.println("Erreur d'inscription : " + err.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Erreur d'inscription").encode());
});
}
public void handleLogin(RoutingContext context) {
JsonObject body = context.body().asJsonObject();
if (body == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Requête invalide").encode());
return;
}
String email = body.getString("email");
String password = body.getString("password");
if (email == null || password == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Email et mot de passe requis").encode());
return;
}
databaseService.pool
.preparedQuery("SELECT id, name, surname, password, points FROM users WHERE email = ?")
.execute(Tuple.of(email))
.onSuccess(result -> {
if (result.rowCount() == 0) {
context.response()
.setStatusCode(401)
.end(new JsonObject().put("error", "Email ou mot de passe incorrect").encode());
return;
}
// Récupération de la ligne de manière sécurisée
Row row = null;
try {
row = result.iterator().next();
} catch (Exception e) {
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Erreur lors de la récupération des données utilisateur").encode());
return;
}
// Vérification que row n'est pas null avant d'y accéder
if (row == null) {
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Données utilisateur introuvables").encode());
return;
}
// Récupération des champs de manière sécurisée
Integer id = row.getInteger("id");
String storedHashedPassword = row.getString("password");
Integer nbPointsUser = row.getInteger("points");
String name = row.getString("name");
String surname = row.getString("surname");
// Vérification des champs obligatoires
if (id == null || storedHashedPassword == null) {
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Données utilisateur incomplètes").encode());
return;
}
// Valeur par défaut pour les points si null
if (nbPointsUser == null) {
nbPointsUser = 0;
}
BCrypt.Result verification = BCrypt.verifyer().verify(password.toCharArray(), storedHashedPassword);
if (verification.verified) {
JsonObject claims = new JsonObject()
.put("sub", email)
.put("id", id);
// Ajout de name et surname seulement s'ils ne sont pas null
if (name != null) claims.put("name", name);
if (surname != null) claims.put("surname", surname);
// Attribution du rôle en fonction des points
String role = "user";
if (nbPointsUser >= 200) {
role = "admin";
} else if (nbPointsUser >= 100) {
role = "complexe";
}
claims.put("role", role);
String token = jwtAuth.generateToken(claims);
context.response()
.setStatusCode(200)
.end(new JsonObject().put("token", token).encode());
} else {
context.response()
.setStatusCode(401)
.end(new JsonObject().put("error", "Email ou mot de passe incorrect").encode());
}
})
.onFailure(err -> {
System.err.println("Erreur de connexion : " + err.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject().put("error", "Erreur serveur").encode());
});
}
}

View File

@ -0,0 +1,23 @@
package com.example.starter;
import io.vertx.core.Vertx;
import io.vertx.jdbcclient.JDBCConnectOptions;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.PoolOptions;
public class DatabaseService {
JDBCPool pool;
public DatabaseService(Vertx vertx) {
pool = JDBCPool.pool(vertx,
new JDBCConnectOptions()
.setJdbcUrl("jdbc:postgresql://" + (System.getenv("DB_HOST") != null ? System.getenv("DB_HOST") : "localhost") + ":" + (System.getenv("DB_PORT") != null ? System.getenv("DB_PORT") : "5432") + "/" + (System.getenv("DB_NAME") != null ? System.getenv("DB_NAME") : "postgres") + "?useUnicode=true&characterEncoding=UTF-8")
.setUser(System.getenv("DB_USER") != null ? System.getenv("DB_USER") : "postgres")
.setPassword(System.getenv("DB_PASSWORD") != null ? System.getenv("DB_PASSWORD") : "admin"),
new PoolOptions()
.setName("")
.setMaxSize(16)
);
}
}

View File

@ -0,0 +1,18 @@
package com.example.starter;
import io.vertx.core.Vertx;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.auth.KeyStoreOptions;
import com.example.starter.JwtAuthProvider;
public class JwtAuthProvider {
public static JWTAuth createJwtAuth(Vertx vertx) {
return JWTAuth.create(vertx, new JWTAuthOptions()
.setKeyStore(new KeyStoreOptions()
.setPath("keystore.jceks")
.setPassword("secret")));
}
}

View File

@ -0,0 +1,96 @@
package com.example.starter;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.web.handler.JWTAuthHandler;
public class MainVerticle extends AbstractVerticle {
private DatabaseService databaseService;
private Router router;
@Override
public void start(Promise<Void> startPromise) throws Exception {
databaseService = new DatabaseService(vertx);
// Initialisation du fournisseur JWT
JWTAuth jwtAuth = JwtAuthProvider.createJwtAuth(vertx);
// Initialisation du routeur
router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(CorsHandler.create()
.addOrigin("*")
.allowedMethod(HttpMethod.GET)
.allowedMethod(HttpMethod.POST)
.allowedHeader("Content-Type")
.allowedHeader("Authorization"));
// Protéger toutes les routes commençant par "/api/"
router.route("/api/*").handler(JWTAuthHandler.create(jwtAuth));
// Initialisation des handlers de requêtes
QueryObjects queryObjects = new QueryObjects(databaseService);
QueryWeatherData queryWeather = new QueryWeatherData(databaseService);
SetObjects setObjects = new SetObjects(databaseService);
SetWeatherData setWeatherData = new SetWeatherData(databaseService);
AuthHandler authHandler = new AuthHandler(databaseService, jwtAuth);
QueryUsers queryUsers = new QueryUsers(databaseService);
SetUser setUser = new SetUser(databaseService);
setObjects.setUserHandler(setUser);
queryObjects.setUserHandler(setUser);
setWeatherData.setUserHandler(setUser);
QueryDeleteObject RequestDeleteObject = new QueryDeleteObject(databaseService);
SetRequestDeleteObject DeleteObject = new SetRequestDeleteObject(databaseService);
// Déclaration des routes
router.get("/objets").handler(queryObjects::getObjects);
router.post("/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);
router.get("/getRange").handler(queryWeather::getRangeData);
router.post("/modifRangeData").handler(setWeatherData::setRangeData);
router.post("/deleteObject").handler(setObjects::deleteObject);
router.get("/users").handler(queryUsers::getUsers);
router.post("/user").handler(queryUsers::getUser);
router.post("/setUserPoints").handler(setUser::setUserPoints);
router.post("/deleteUser").handler(setUser::deleteUser);
router.post("/updateProfil").handler(setUser::updateUserProfile);
router.post("/changePassword").handler(setUser::changeUserPassword);
router.post("/publicUser").handler(queryUsers::getPublicUser);
router.get("/getCategories").handler(queryObjects::getCategories);
router.get("/getLocations").handler(queryObjects::getLocations);
router.post("/getMeteoHome").handler(queryWeather::getMeteoInfosHomePage);
router.post("/addCategories").handler(setObjects::newCategorie);
router.post("/deleteCategories").handler(setObjects::deleteCategories);
router.post("/requestDeleteObject").handler(RequestDeleteObject::deleteObject);
router.get("/getDemandeSuppression").handler(DeleteObject::getAllDeletionRequests);
router.post("/reject").handler(RequestDeleteObject::rejectDemande);
// Routes d'authentification
router.post("/signup").handler(authHandler::handleSignup);
router.post("/login").handler(authHandler::handleLogin);
// Création du serveur HTTP
vertx.createHttpServer()
.requestHandler(router)
.listen(8888)
.onSuccess(server -> {
System.out.println("HTTP server started on port " + server.actualPort());
startPromise.complete();
})
.onFailure(throwable -> {
throwable.printStackTrace();
startPromise.fail(throwable);
});
}
}

View File

@ -0,0 +1,144 @@
package com.example.starter;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Tuple;
public class QueryDeleteObject {
private DatabaseService databaseService;
public QueryDeleteObject(DatabaseService dtbS) {
this.databaseService = dtbS;
}
public void deleteObject(RoutingContext context) {
JsonObject body = context.body().asJsonObject();
if (body == null) {
context.response().setStatusCode(400)
.end(new JsonObject().put("error", "Requête invalide").encode());
return;
}
Integer objectId = body.getInteger("object_id");
Integer userId = body.getInteger("requested_by");
if (objectId == null || userId == null) {
context.response().setStatusCode(400)
.end(new JsonObject().put("error", "Champs manquants").encode());
return;
}
String checkQuery = "SELECT id FROM weather_objects WHERE id = ?";
databaseService.pool.preparedQuery(checkQuery).execute(Tuple.of(objectId), res -> {
if (res.failed()) {
res.cause().printStackTrace();
context.response().setStatusCode(500)
.end(new JsonObject().put("error", "Erreur base de données").encode());
return;
}
if (res.result().rowCount() == 0) {
context.response().setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
String checkDuplicate = "SELECT 1 FROM deletion_requests WHERE object_id = ? AND requested_by = ?";
databaseService.pool.preparedQuery(checkDuplicate).execute(Tuple.of(objectId, userId), duplicateCheck -> {
if (duplicateCheck.failed()) {
duplicateCheck.cause().printStackTrace();
context.response().setStatusCode(500)
.end(new JsonObject().put("error", "Erreur vérification duplication").encode());
return;
}
if (duplicateCheck.result().rowCount() > 0) {
context.response().setStatusCode(409)
.end(new JsonObject().put("error", "Demande déjà existante").encode());
return;
}
String insertQuery = "INSERT INTO deletion_requests (object_id, requested_by) VALUES (?, ?)";
databaseService.pool.preparedQuery(insertQuery).execute(Tuple.of(objectId, userId), insertRes -> {
if (insertRes.succeeded()) {
context.response().setStatusCode(200)
.end(new JsonObject().put("message", "Demande envoyée").encode());
} else {
insertRes.cause().printStackTrace();
context.response().setStatusCode(500)
.end(new JsonObject().put("error", "Erreur insertion").encode());
}
});
});
});
}
public void getAllDeletionRequests(RoutingContext context) {
String query = "SELECT * FROM deletion_requests";
databaseService.pool.query(query).execute(res -> {
if (res.succeeded()) {
var results = res.result();
var jsonArray = new io.vertx.core.json.JsonArray();
results.forEach(row -> {
JsonObject json = new JsonObject()
.put("id", row.getInteger("id"))
.put("object_id", row.getInteger("object_id"))
.put("requested_by", row.getInteger("requested_by"))
.put("requested_at", row.getLocalDateTime("requested_at").toString());
jsonArray.add(json);
});
context.response()
.putHeader("Content-Type", "application/json")
.end(jsonArray.encode());
} else {
res.cause().printStackTrace();
context.response().setStatusCode(500)
.end(new JsonObject().put("error", "Erreur lors de la récupération des demandes").encode());
}
});
}
public void rejectDemande(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");
databaseService.pool
.preparedQuery("DELETE FROM deletion_requests WHERE id=?")
.execute(Tuple.of(id))
.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", "Demande non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "La demande à bien été supprimé").encode());
return;
});
}
}

View File

@ -0,0 +1,162 @@
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;
}
private SetUser setUser;
public void setUserHandler(SetUser setUser) {
this.setUser = setUser;
}
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 getLocations(RoutingContext context) {
databaseService.pool
.query("SELECT DISTINCT location 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 -> {
if (!rows.iterator().hasNext()) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Aucun lieu trouvé dans la base de données").encode());
return;
}
JsonArray types = new JsonArray();
rows.forEach(row -> types.add(row.getString("location")));
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(types.encode());
});
}
public void getCategories(RoutingContext context) {
databaseService.pool
.query("SELECT name FROM categories;")
.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 -> {
if (!rows.iterator().hasNext()) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Aucun type trouvé dans la base de données").encode());
return;
}
JsonArray types = new JsonArray();
rows.forEach(row -> types.add(row.getString("name")));
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(types.encode());
});
}
public void getParticularObject(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 id = body.getString("id");
String idUser = body.getString("userId");
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;
}
Boolean shouldUpdatePoints = body.getBoolean("shouldUpdatePoints", false);
if (Boolean.TRUE.equals(shouldUpdatePoints) && idUser != null) {
setUser.updateUserPoints(Integer.parseInt(idUser), 1);
}
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"))
.put("proprio_id", row.getInteger("proprio_id"));
objects.add(object);
}
return objects;
}
}

View File

@ -0,0 +1,151 @@
package com.example.starter;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
public class QueryUsers {
private DatabaseService databaseService;
public QueryUsers(DatabaseService dtbS) {
this.databaseService = dtbS;
}
public void getUsers(RoutingContext context) {
databaseService.pool
.query("SELECT * FROM users;")
.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 -> {
JsonArray users = new JsonArray();
for (Row row : rows) {
int points = row.getInteger("points");
JsonObject user = new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("surname", row.getString("surname"))
.put("email", row.getString("email"))
.put("gender", row.getString("gender"))
.put("pseudo",row.getString("pseudo"))
.put("points", points);
if (points <= 60) {
user.put("role", "user");
} else if (points <= 100) {
user.put("role", "complexe");
} else if (points >= 200) {
user.put("role", "admin");
}
users.add(user);
}
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(users.encode());
});
}
public void getUser(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 idUser = body.getInteger("id");
databaseService.pool
.preparedQuery("SELECT * FROM users WHERE id=?;")
.execute(Tuple.of(idUser))
.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.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
return;
}
Row row = rows.iterator().next();
int points = row.getInteger("points");
JsonObject user = new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("surname", row.getString("surname"))
.put("email", row.getString("email"))
.put("gender", row.getString("gender"))
.put("pseudo",row.getString("pseudo"))
.put("points", points);
if (points <= 60) {
user.put("role", "user");
} else if (points <= 100) {
user.put("role", "complexe");
} else if (points >= 200) {
user.put("role", "admin");
}
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(user.encode());
});
}
public void getPublicUser(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 idUser = body.getInteger("id");
databaseService.pool
.preparedQuery("SELECT * FROM users WHERE id=?;")
.execute(Tuple.of(idUser))
.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.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Utilisateur non trouvé").encode());
return;
}
Row row = rows.iterator().next();
int points = row.getInteger("points");
JsonObject user = new JsonObject()
.put("id", row.getInteger("id"))
.put("gender", row.getString("gender"))
.put("pseudo",row.getString("pseudo"))
.put("points", points);
if (points <= 60) {
user.put("role", "user");
} else if (points <= 100) {
user.put("role", "complexe");
} else if (points >= 200) {
user.put("role", "admin");
}
context.response()
.putHeader("content-type", "application/json; charset=UTF-8")
.end(user.encode());
});
}
}

View File

@ -0,0 +1,166 @@
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 getMeteoInfosHomePage(RoutingContext context) {
JsonObject body = context.body().asJsonObject();
String location = body.getString("location");
if (location == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Paramètre 'location' manquant").encode());
return;
}
databaseService.pool
.query("SELECT wd.wind_speed, wd.temperature, wd.humidity, wd.pressure FROM weather_data wd " +
"JOIN weather_objects s ON wd.station_id = s.id WHERE s.location = '" + location +
"' ORDER BY wd.timestamp DESC LIMIT 1;")
.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 -> {
if (rows.size() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Aucune donnée trouvée pour cette location").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());
});
}
public void getRangeData(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_min,temperature_max,pressure_min,pressure_max,humidity_min,humidity_max FROM range_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("error", "Erreur de récupération de la BDD").encode());
return;
})
.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,207 @@
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;
private SetUser setUser;
public SetObjects(DatabaseService ddbs) {
this.databaseService = ddbs;
}
public void setUserHandler(SetUser setUser) {
this.setUser = setUser;
}
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");
Integer idUser = body.getInteger("idUser");
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;
}
Boolean shouldUpdatePoints = body.getBoolean("shouldUpdatePoints", false);
if (Boolean.TRUE.equals(shouldUpdatePoints) && idUser != null) {
setUser.updateUserPoints(idUser, 1);
}
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 deleteCategories(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("name");
databaseService.pool
.preparedQuery("DELETE FROM categories WHERE name=?")
.execute(Tuple.of(name))
.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", "Catégorie non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "La catégorie à bien été supprimé").encode());
return;
});
}
public void deleteObject(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 id = body.getString("id");
databaseService.pool
.preparedQuery("DELETE 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("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é supprimé").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;
}
Integer idUser = body.getInteger("proprio_id");
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");
String batterieType = body.getString("batterieType");
Integer proprio_id = body.getInteger("proprio_id");
databaseService.pool
.preparedQuery(
"INSERT INTO weather_objects (name,description,type,location,status,type_batterie,proprio_id) VALUES (?,?,?,?,?,?,?)")
.execute(Tuple.of(name, description, type, location, status, batterieType, proprio_id))
.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;
}
if (idUser != null) {
setUser.updateUserPoints(idUser, 2);
}
;
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "L'objet à bien été ajouté").encode());
return;
});
}
public void newCategorie(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("name");
databaseService.pool
.preparedQuery(
"INSERT INTO categories (name) VALUES (?)")
.execute(Tuple.of(name))
.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", "La catégorie à bien été ajouté").encode());
return;
});
}
}

View File

@ -0,0 +1,43 @@
package com.example.starter;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
public class SetRequestDeleteObject {
private final DatabaseService databaseService;
// Le nom du constructeur doit correspondre au nom de la classe
public SetRequestDeleteObject(DatabaseService dtbS) {
this.databaseService = dtbS;
}
public void getAllDeletionRequests(RoutingContext context) {
String query = "SELECT * FROM deletion_requests";
databaseService.pool.query(query).execute(res -> {
if (res.succeeded()) {
RowSet<Row> results = res.result();
JsonArray jsonArray = new JsonArray();
results.forEach(row -> {
JsonObject json = new JsonObject()
.put("id", row.getInteger("id"))
.put("object_id", row.getInteger("object_id"))
.put("requested_by", row.getInteger("requested_by"))
.put("request_date", row.getLocalDateTime("request_date").toString()); // Assurez-vous que le type correspond
jsonArray.add(json);
});
context.response()
.putHeader("Content-Type", "application/json")
.end(jsonArray.encode());
} else {
res.cause().printStackTrace();
context.response().setStatusCode(500).end(new JsonObject().put("error", "Erreur lors de la récupération des demandes").encode());
}
});
}
}

View File

@ -0,0 +1,196 @@
package com.example.starter;
import at.favre.lib.crypto.bcrypt.BCrypt;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Tuple;
public class SetUser {
private DatabaseService databaseService;
public SetUser(DatabaseService ddbs) {
this.databaseService = ddbs;
}
public void updateUserPoints(Integer userId, Integer points) {
databaseService.pool
.preparedQuery("UPDATE users SET points=points+? WHERE id=?")
.execute(Tuple.of(points, userId))
.onFailure(e -> {
System.err.println("Erreur de mise à jour des points :" + e.getMessage());
})
.onSuccess(rows -> {
if (rows.rowCount() > 0) {
System.out.println("Points de l'utilisateur mis à jour avec succès");
} else {
System.out.println("Utilisateur non trouvé pour la mise à jour des points");
}
});
}
public void changeUserPassword(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 oldPassword = body.getString("oldPassword");
String newPassword = body.getString("newPassword");
databaseService.pool
.preparedQuery("SELECT password FROM users WHERE id=?")
.execute(Tuple.of(id))
.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", "Utilisateur non trouvé").encode());
return;
}
String currentPassword = rows.iterator().next().getString("password");
BCrypt.Result verification = BCrypt.verifyer().verify(oldPassword.toCharArray(), currentPassword);
if (!verification.verified) {
context.response()
.setStatusCode(401)
.end(new JsonObject().put("error", "Ancien mot de passe incorrect").encode());
return;
}
String hashedPassword = BCrypt.withDefaults().hashToString(12, newPassword.toCharArray());
databaseService.pool
.preparedQuery("UPDATE users SET password=? WHERE id=?")
.execute(Tuple.of(hashedPassword, id))
.onFailure(e -> {
System.err.println("Erreur lors de la mise à jour du mot de passe :" + e.getMessage());
context.response()
.setStatusCode(500)
.end(new JsonObject()
.put("error", "Erreur lors de la mise à jour du mot de passe")
.encode());
})
.onSuccess(updateRows -> {
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "Le mot de passe a bien été mis à jour")
.encode());
});
});
}
public void updateUserProfile(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 name = body.getString("name");
String surname = body.getString("surname");
String pseudo = body.getString("pseudo");
databaseService.pool
.preparedQuery("UPDATE users SET name=?, surname=?, pseudo=? WHERE id=?")
.execute(Tuple.of(name, surname,pseudo, id))
.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", "Utilisateur non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject()
.put("success", "Les informations de l'utilisateur ont bien été mises à jour")
.encode());
return;
});
}
public void setUserPoints(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");
Integer points = body.getInteger("points");
databaseService.pool
.preparedQuery(
"UPDATE users SET points=? WHERE id=?")
.execute(Tuple.of(points, 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", "Utilisateur non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "Les points de l'utilisateur ont bien été mis à jour")
.encode());
return;
});
}
public void deleteUser(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");
databaseService.pool
.preparedQuery("DELETE FROM users WHERE id=?")
.execute(Tuple.of(id))
.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", "Utilisateur non trouvé").encode());
return;
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "L'utilisateur à bien été supprimé").encode());
return;
});
}
}

View File

@ -0,0 +1,72 @@
package com.example.starter;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Tuple;
public class SetWeatherData {
private DatabaseService databaseService;
private SetUser setUser;
public SetWeatherData(DatabaseService ddbs) {
this.databaseService = ddbs;
}
public void setUserHandler(SetUser setUser) {
this.setUser = setUser;
}
public void setRangeData(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 id = body.getString("id");
Double min = body.getDouble("min");
Double max = body.getDouble("max");
String type = body.getString("type");
if (id == null || min == null || max == null || type == null) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Paramètres manquants ou invalides").encode());
return;
}
if (!type.matches("^[a-zA-Z_]+$")) {
context.response()
.setStatusCode(400)
.end(new JsonObject().put("error", "Type invalide").encode());
return;
}
String query = String.format("UPDATE range_data SET %s_min=?, %s_max=? WHERE station_id=?", type, type);
Integer idUser = body.getInteger("idUser");
databaseService.pool
.preparedQuery(
query)
.execute(Tuple.of(min, max, 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.rowCount() == 0) {
context.response()
.setStatusCode(404)
.end(new JsonObject().put("error", "Objet non trouvé").encode());
return;
}
if (idUser != null) {
setUser.updateUserPoints(idUser, 1);
}
context.response()
.putHeader("content-type", "application/json: charset=UTF-8")
.end(new JsonObject().put("success", "Les limites ont bien été mis à jour").encode());
return;
});
}
}

View File

@ -0,0 +1,22 @@
package com.example.starter;
import io.vertx.core.Vertx;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(VertxExtension.class)
public class TestMainVerticle {
@BeforeEach
void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new MainVerticle()).onComplete(testContext.succeeding(id -> testContext.completeNow()));
}
@Test
void verticle_deployed(Vertx vertx, VertxTestContext testContext) throws Throwable {
testContext.completeNow();
}
}

24
z1/Front-end/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

25
z1/Front-end/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# Stage 1: Build the React application
FROM node:18-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the application using Nginx
FROM nginx:alpine
# Remove default Nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Copy built app from the first stage
COPY --from=build /app/dist /usr/share/nginx/html
# Replace the default Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,27 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default [
{ ignores: ['dist'] },
js.configs.recommended,
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
];

14
z1/Front-end/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href=".//img/cloud-sun-rain.svg"/>
<title>Projet Dev Web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

12
z1/Front-end/nginx.conf Normal file
View File

@ -0,0 +1,12 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Fallback to index.html for Single Page Applications handling client-side routing
location / {
try_files $uri $uri/ /index.html;
}
}

55
z1/Front-end/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "vite-react-javascript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^7.0.1",
"axios": "^1.8.4",
"i18next": "^26.0.1",
"i18next-browser-languagedetector": "^8.2.1",
"jwt-decode": "^4.0.0",
"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-i18next": "^17.0.1",
"react-router-dom": "^7.4.0",
"recharts": "^2.15.1"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.23.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.11",
"globals": "^15.9.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.17",
"vite": "^5.4.2"
},
"description": "Bienvenue dans le projet **DevWeb** ! Ce projet utilise **Vite** et **React** pour créer une application web moderne et performante.",
"main": "eslint.config.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Charles40130/Projet-Dev-Web-Ing1.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Charles40130/Projet-Dev-Web-Ing1/issues"
},
"homepage": "https://github.com/Charles40130/Projet-Dev-Web-Ing1#readme"
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-sun-rain-icon lucide-cloud-sun-rain"><path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"/><path d="M11 20v2"/><path d="M7 19v2"/></svg>

After

Width:  |  Height:  |  Size: 494 B

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: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

54
z1/Front-end/src/App.jsx Normal file
View File

@ -0,0 +1,54 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./AuthContext.jsx";
import Home from "./pages/Home.jsx";
import About from "./pages/About.jsx";
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";
import Profil from "./pages/Profil.jsx";
import Sidebar from "./pages/Admin/sidebar.jsx";
import User from "./pages/Admin/User.jsx";
import Dashboard from "./pages/Admin/Dashboard.jsx";
import AdminObjet from "./pages/Admin/AdminObjet.jsx";
import ProtectedRoute from "./ProtectedRoute.jsx";
function App() {
return (
<AuthProvider>
<Router>
<div>
<Header />
<Routes>
{/* Routes publiques */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
{/* Routes protégées pour tous les utilisateurs connectés */}
<Route path="/gestion" element={<ProtectedRoute element={<Gestion />} allowedRoles={['admin', 'complexe', 'user']} />} />
<Route path="/gestionObjets" element={<ProtectedRoute element={<ObjectManagement />} allowedRoles={['admin', 'complexe', 'user']} />} />
<Route path="/objet" element={<ProtectedRoute element={<Objet />} allowedRoles={['admin', 'complexe', 'user']} />} />
{/* Routes protégées pour les admins et complexes */}
<Route path="/ajouterObjet" element={<ProtectedRoute element={<AddObject />} allowedRoles={['admin', 'complexe']} />} />
<Route path="/profil" element={<ProtectedRoute element={<Profil />} allowedRoles={['admin', 'complexe','user']} />} />
{/* Routes protégées pour tous les utilisateurs connectés */}
<Route path="/sidebar" element={<ProtectedRoute element={<Sidebar />} allowedRoles={['admin', 'complexe', 'user']} />} />
<Route path="/user" element={<ProtectedRoute element={<User />} allowedRoles={['admin', 'complexe', 'user']} />} />
{/* Routes protégées pour les admins uniquement */}
<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} allowedRoles={['admin']} />} />
<Route path="/adminobjet" element={<ProtectedRoute element={<AdminObjet />} allowedRoles={['admin']} />} />
</Routes>
</div>
</Router>
</AuthProvider>
);
}
export default App;

View File

@ -0,0 +1,47 @@
import React, { createContext, useContext, useState, useEffect } from "react";
import { jwtDecode } from "jwt-decode";
// Créer le contexte
const AuthContext = createContext();
// Hook pour accéder facilement au contexte
export const useAuth = () => useContext(AuthContext);
// Fournisseur de contexte qui gère l'état du token et de l'utilisateur
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem("token"));
const [user, setUser] = useState(null);
// Met à jour le token et décode l'utilisateur
useEffect(() => {
if (token) {
try {
const decoded = jwtDecode(token);
setUser(decoded);
} catch (error) {
console.error("Erreur lors du décodage du token:", error);
setUser(null);
}
} else {
setUser(null);
}
}, [token]);
const login = (newToken) => {
localStorage.setItem("token", newToken);
setToken(newToken);
};
const logout = () => {
localStorage.removeItem("token");
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ token, user, login, logout }}>
{children}
</AuthContext.Provider>
);
};

View File

@ -0,0 +1,20 @@
import React from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext"; // Utilisation du contexte d'authentification
function ProtectedRoute({ element, allowedRoles }) {
const { token, user } = useAuth(); // Vérifier si un token existe, donc si l'utilisateur est authentifié
// Si l'utilisateur n'est pas authentifié, redirigez-le vers la page de login
if (!token) {
return <Navigate to="/login" />;
}
if(user){
if (allowedRoles && !allowedRoles.includes(user?.role)) {
return <Navigate to="/" />;
}
return element;
}
}
export default ProtectedRoute;

View File

@ -0,0 +1,33 @@
import React, { useEffect } from "react";
import { Check, X } from "lucide-react";
import { useAuth } from "../AuthContext";
function Alert({ affAlert, setAffAlert, message }) {
const { user } = useAuth();
useEffect(() => {
if (affAlert) {
const timer = setTimeout(() => {
setAffAlert(false);
}, 3000);
return () => clearTimeout(timer);
}
}, [affAlert, setAffAlert]);
return (
affAlert && user?.role !== "user" && (
<div className="fixed top-6 right-4 z-50 bg-slate-700 text-white px-6 py-4 rounded-xl shadow-lg flex items-center gap-4 w-[90%] sm:w-[350px]">
<Check className="text-green-400 w-6 h-6 flex-shrink-0" />
<p className="text-sm sm:text-base flex-grow">{message}</p>
<button
onClick={() => setAffAlert(false)}
className="text-white hover:text-gray-300"
>
<X />
</button>
</div>
)
);
}
export default Alert;

View File

@ -0,0 +1,23 @@
import React, {useState} from "react";
import { TriangleAlert,X } from "lucide-react";
import { useAuth } from "../AuthContext";
function AlertInactive({affAlert,setAffAlert, message}) {
const { user } = useAuth();
return (
(affAlert&&(user?.role!=="user")&&(
<div className="fixed z-50 flex flex-col md:flex-row bg-slate-600 w-full md:w-1/2 lg:w-1/3 top-20 right-1 sm: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">
{message}
</p>
</div>
)));
}
export default AlertInactive;

View File

@ -0,0 +1,29 @@
import React from "react";
import { Battery } from "lucide-react";
import Progress from "react-circle-progress-bar";
import { useTranslation } from "react-i18next";
function BatterieInfo({ object }) {
const { t } = useTranslation();
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 ">
{t('components.batterieInfo.title')}
</h1>
</div>
<div className="flex flex-col items-center">
<Progress progress={object.batterie} />
<h1 className="font-bold">
{t('components.batterieInfo.batteryType')}{" "}
<span className="capitalize font-normal">{object.type_batterie}</span>
</h1>
</div>
</div>
);
}
export default BatterieInfo;

View File

@ -0,0 +1,25 @@
import React from "react";
import { ChartLine } from "lucide-react";
function BoutonGraphique({ type, setGraphStates, graphStates, graphCible }) {
const handleClick = () => {
setGraphStates((prev) => ({ ...prev, [type]: !prev[type] }));
};
return !graphStates[type] ? (
<button
className="bg-blue-200 py-2 my-2 px-4 rounded-full mr-2"
onClick={handleClick}
>
<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}
>
<ChartLine className="text-indigo-600" size={24} />
</button>
);
}
export default BoutonGraphique;

View File

@ -0,0 +1,280 @@
import React, { useEffect, useState } from "react";
import { BadgePlus } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
import { useAuth } from "../AuthContext";
import { useTranslation } from "react-i18next";
function FormNewObject({ isAdmin }) {
const { t } = useTranslation();
const { user } = useAuth();
const [categorie, setCategorie] = useState();
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [location, setLocalisation] = useState("");
const [proprio_id, setProprio_id] = useState(user?.id);
const [batterieType, setBatterieType] = 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,
batterieType,
proprio_id,
})
.then((response) => {
setMessRequete(t('components.formNewObject.successRecord'));
setEnregistre(true);
console.log("Ajout de l'objet réussit :", response.data);
})
.catch((error) => {
setMessRequete(t('components.formNewObject.errorRecord'));
console.error("Erreur lors de l'ajout de l'objet :", error);
});
setVerif(false);
resetForm();
} else {
setVerif(true);
}
}
useEffect(() => {
axios
.get(`${API_BASE_URL}/getCategories`)
.then((response) => {
if (response.data.length === 0) {
console.warn(t('components.formNewObject.noCategory'));
} else {
setCategorie(response.data);
}
})
.catch((error) => {
console.error("Erreur lors de la récupération des catégories :", error);
});
}, []);
function resetForm() {
setNom("");
setStatus("active");
setDescription("");
setType("");
setLocalisation("");
setBatterieType("");
if (isAdmin) set_id("");
setActive(true);
}
function handleCancel() {
if (verif) {
setVerif(false);
} else {
resetForm();
}
}
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">
{isAdmin ? (
<h2 className="text-2xl font-semibold mb-3">
{t('components.formNewObject.addTitle')}
</h2>
) : (
<>
<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
? t('components.formNewObject.enterData')
: t('components.formNewObject.confirmData')}
</h1>
</>
)}
</div>
<div className="mb-5">
<label
htmlFor="nom"
className="block mb-2 text-sm font-medium text-gray-900"
>
{t('components.formNewObject.name')}
</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"
>
{t('components.formNewObject.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"
>
{t('components.formNewObject.type')}
</label>
<select
id="type"
className="text-gray-600 border rounded-lg p-2 w-full"
value={type}
onChange={(e) => setType(e.target.value)}
required
disabled={verif}
>
<option value="">{t('components.formNewObject.selectType')}</option>
{categorie&&categorie.map((cat, index) => (
<option key={index} value={cat}>
{cat}
</option>
))}
</select>
</div>
<div className="mb-5">
<label
htmlFor="location"
className="block mb-2 text-sm font-medium text-gray-900"
>
{t('components.formNewObject.location')}
</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
htmlFor="batterieType"
className="block mb-2 text-sm font-medium text-gray-900"
>
{t('components.formNewObject.batteryType')}
</label>
<input
id="batterieType"
className="text-gray-600 border rounded-lg p-2 w-full"
type="text"
value={batterieType}
onChange={(e) => setBatterieType(e.target.value)}
required
disabled={verif}
/>
</div>
<div className="mb-5">
<label
htmlFor="proprio_id"
className="block mb-2 text-sm font-medium text-gray-900"
>
{t('components.formNewObject.owner')}
</label>
<input
id="proprio_id"
className="text-gray-600 border rounded-lg p-2 w-full"
type="number"
value={proprio_id}
onChange={(e) => setProprio_id(e.target.value)}
required
disabled={verif || !isAdmin}
/>
</div>
<div className="mb-5">
<label className="block mb-2 text-sm font-medium text-gray-900">
{t('components.formNewObject.status')}
</label>
<div className="inline-flex items-center gap-2">
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
{t('components.formNewObject.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"
>
{t('components.formNewObject.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 ? t('components.formNewObject.confirmInfos') : t('components.formNewObject.sureBtn')}
</button>
<button
type="button"
className="text-red-500 hover:cursor-pointer hover:underline"
onClick={handleCancel}
>
{!verif ? t('components.formNewObject.deleteInfos') : t('components.formNewObject.changeBtn')}
</button>
</div>
<p className={enregistre ? "text-green-700" : "text-red-700"}>
{messRequete}
</p>
</form>
);
}
export default FormNewObject;

View File

@ -0,0 +1,230 @@
import React, { useState, useEffect } from "react";
import { X, Menu, LogIn, UserPlus, LogOut, User } from "lucide-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useAuth } from "../AuthContext";
import LanguageSwitcher from "./LanguageSwitcher";
function Header() {
const { t } = useTranslation();
const { token, user, logout } = useAuth();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [showAdminDropdown, setShowAdminDropdown] = useState(false);
const toggleAdminDropdown = () => {
setShowAdminDropdown((prev) => !prev);
};
return (
<header className="bg-white shadow-md relative">
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center sm:grid sm:grid-cols-3">
{/* Logo Section */}
<div className="flex justify-start">
<Link to="/" className="text-2xl font-bold text-indigo-600">
VigiMétéo
</Link>
</div>
{/* Navigation Section (Centered on Desktop) */}
<nav
className={`${
isMenuOpen ? "block" : "hidden"
} absolute z-[1000] top-16 left-0 w-full bg-white shadow-md sm:static sm:flex sm:justify-center sm:shadow-none sm:bg-transparent`}
>
<ul className="flex flex-col sm:flex-row gap-4 sm:gap-10 text-gray-600 p-4 sm:p-0 font-medium">
<li>
<Link
to="/"
onClick={() => setIsMenuOpen(false)}
className="text-gray-600 hover:text-indigo-600 transition-colors"
>
{t('header.home')}
</Link>
</li>
<li>
<Link
to="/about"
onClick={() => setIsMenuOpen(false)}
className="text-gray-600 hover:text-indigo-600 transition-colors"
>
{t('header.about')}
</Link>
</li>
{/* Logic for roles */}
{token && (
<>
{user?.role === "user" ? (
<li>
<Link
to="/gestionObjets"
onClick={() => setIsMenuOpen(false)}
className="text-gray-600 hover:text-indigo-600 transition-colors"
>
{t('header.visualizer')}
</Link>
</li>
) : (
<li>
<Link
to="/gestion"
onClick={() => setIsMenuOpen(false)}
className="text-gray-600 hover:text-indigo-600 transition-colors"
>
{t('header.manage')}
</Link>
</li>
)}
{user?.role === "admin" && (
<li className="relative group">
<button
onClick={toggleAdminDropdown}
className="flex items-center text-gray-600 hover:text-indigo-600 focus:outline-none transition-colors"
>
{t('header.admin')}
<svg
className="ml-1 h-4 w-4 fill-current"
viewBox="0 0 20 20"
>
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
</button>
{showAdminDropdown && (
<div className="absolute top-full left-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-50">
<Link
to="/dashboard"
className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
onClick={() => {setShowAdminDropdown(false);setIsMenuOpen(false);}}
>
{t('header.dashboard')}
</Link>
<Link
to="/user"
className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
onClick={() => {setShowAdminDropdown(false);setIsMenuOpen(false);}}
>
{t('header.manageUsers')}
</Link>
<Link
to="/adminobjet"
className="block px-4 py-2 text-gray-700 hover:bg-gray-100"
onClick={() => {setShowAdminDropdown(false);setIsMenuOpen(false);}}
>
{t('header.manageObjects')}
</Link>
</div>
)}
</li>
)}
</>
)}
{/* Mobile-only auth links */}
{!token ? (
<>
<li className="sm:hidden">
<Link
to="/login"
onClick={() => setIsMenuOpen(false)}
className="hover:text-indigo-600 flex items-center gap-2"
>
<LogIn size={20} />
{t('header.login')}
</Link>
</li>
<li className="sm:hidden">
<Link
to="/signup"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg"
>
<UserPlus size={20} />
{t('header.signup')}
</Link>
</li>
</>
) : (
<>
<li className="sm:hidden">
<Link
to="/profil"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2 text-gray-600 hover:text-indigo-600"
>
<User size={20} />
<span>{t('header.profile')}</span>
</Link>
</li>
<li className="sm:hidden">
<button
onClick={() => {
logout();
setIsMenuOpen(false);
}}
className="flex items-center gap-2 text-gray-600 hover:text-red-600 w-full text-left"
>
<LogOut size={20} />
<span>{t('header.logout')}</span>
</button>
</li>
</>
)}
</ul>
</nav>
{/* Auth Section (Right side) */}
<div className="flex justify-end items-center gap-4">
{!token ? (
<div className="hidden sm:flex gap-4 items-center">
<LanguageSwitcher />
<Link
to="/login"
className="text-gray-600 hover:text-indigo-600 flex items-center gap-2 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<LogIn size={20} />
{t('header.login')}
</Link>
<Link
to="/signup"
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors shadow-sm"
onClick={() => setIsMenuOpen(false)}
>
<UserPlus size={20} />
{t('header.signup')}
</Link>
</div>
) : (
<div className="hidden sm:flex items-center gap-6">
<LanguageSwitcher />
<Link
to="/profil"
className="flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
<User size={20} />
</Link>
<button
onClick={logout}
className="flex items-center gap-2 text-gray-600 hover:text-red-600 transition-colors"
>
<LogOut size={20} />
<span className="font-medium">{t('header.logout')}</span>
</button>
</div>
)}
{/* Mobile Menu Button */}
<button
className="sm:hidden text-gray-600 hover:text-indigo-600 focus:outline-none"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
</header>
);
}
export default Header;

View File

@ -0,0 +1,52 @@
import React from "react";
import { Info } from "lucide-react";
import { useAuth } from "../AuthContext";
import { useTranslation } from "react-i18next";
function InfoObject({ object,defafficherModif }) {
const { t } = useTranslation();
const {user} = useAuth();
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-6 mb-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 ">{t('components.infoObject.title')}</h1>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.infoObject.description')}</p>
<p className="text-gray-600 capitalize">{object.description}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.infoObject.type')}</p>
<p className="text-gray-600 capitalize">{object.type}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.infoObject.location')}</p>
<p className="text-gray-600">{object.location}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.infoObject.status')}</p>
<p className="text-gray-600 capitalize">{object.status}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">
{t('components.infoObject.lastUpdate')}
</p>
<p className="text-gray-600">{object.last_update}</p>
</div>
{user?.role!=="user"&&(
<div className="flex items-center gap-4 mb-1">
<a className="text-blue-500 hover:cursor-pointer" onClick={(()=>defafficherModif(true))}>{t('components.infoObject.modify')}</a>
</div>
)}
</div>
);
}
export default InfoObject;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
};
return (
<div className="flex space-x-2">
<button
onClick={() => changeLanguage('fr')}
className={`px-2 py-1 text-sm rounded transition-colors ${i18n.resolvedLanguage === 'fr' ? 'bg-blue-600 text-white font-bold' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'}`}
>
FR
</button>
<button
onClick={() => changeLanguage('en')}
className={`px-2 py-1 text-sm rounded transition-colors ${i18n.resolvedLanguage === 'en' ? 'bg-blue-600 text-white font-bold' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'}`}
>
EN
</button>
</div>
);
}

View File

@ -0,0 +1,98 @@
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";
import { useTranslation } from "react-i18next";
function MeteoGraph({ object, categorie, Logo,reference}) {
const { t } = useTranslation();
const [rawData, setRawData] = useState([]);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/meteo?id=${identifiant}`).then((response) => {
setRawData(response.data);
});
}, [object]);
useEffect(() => {
if (reference?.current) {
reference.current.scrollIntoView({ behavior: "smooth" });
}
}, [reference]);
function getAvg() {
let moyenne = 0;
rawData.forEach((element) => {
if(element){
moyenne += element[categorie];
}
});
return moyenne / rawData.length;
}
return (
<div
ref={reference}
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 ">
{t('components.meteoGraph.historyTemp')}
</h1>
) : categorie === "humidity" ? (
<h1 className="text-black text-2xl font-bold mb-1 ">
{t('components.meteoGraph.historyHum')}
</h1>
) : (
<h1 className="text-black text-2xl font-bold mb-1 ">
{t('components.meteoGraph.historyPres')}
</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={t('components.meteoGraph.average')} stroke="red" />
</LineChart>
</ResponsiveContainer>
</div>
);
}
export default MeteoGraph;

View File

@ -0,0 +1,84 @@
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";
import ParticularMeteo from "./ParticularMeteo";
import { useTranslation } from "react-i18next";
function MeteoInfos({ object, graphStates, setGraphStates, graphRefs }) {
const { t } = useTranslation();
const [rawData, setRawData] = useState([]);
const [AffAlert, setAffAlert] = useState(false);
const [AffRegles, setAffRegles] = 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;
return (
<div key={object.id} className="bg-white p-6 rounded-xl min-w-5xl">
{AffAlert && object.status === "active" && (
<AlertInactive affAlert={AffAlert} setAffAlert={setAffAlert} message={t('components.meteoInfos.alertInactive')}
/>
)}
<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 ">{t('components.meteoInfos.currentWeather')}</h1>
</div>
{lastData ? (
<div className="flex flex-col items-center gap-4">
<ParticularMeteo
type="temperature"
data={lastData}
Icon={Thermometer}
texte1={t('components.meteoInfos.temperature')}
texte2="°C"
graphStates={graphStates}
setGraphStates={setGraphStates}
graphRefs={graphRefs}
/>
<ParticularMeteo
type="pressure"
data={lastData}
Icon={CircleGauge}
texte1={t('components.meteoInfos.pressure')}
texte2="hPa"
graphStates={graphStates}
setGraphStates={setGraphStates}
graphRefs={graphRefs}
/>
<ParticularMeteo
type="humidity"
data={lastData}
Icon={Droplet}
texte1={t('components.meteoInfos.humidity')}
texte2="%"
graphStates={graphStates}
setGraphStates={setGraphStates}
graphRefs={graphRefs}
/>
<h1 className="text-gray-500 text-sm">
{t('components.meteoInfos.lastRecord')} {lastData.timestamp}
</h1>
</div>
) : (
<p>{t('components.meteoInfos.loading')}</p>
)}
</div>
);
}
export default MeteoInfos;

View File

@ -0,0 +1,164 @@
import React, { useState } from "react";
import { Info } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
import {useAuth} from "../AuthContext";
import { useTranslation } from "react-i18next";
function ModifObject({ object, defafficherModif }) {
const { t } = useTranslation();
const {user}=useAuth();
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();
axios
.post(`${API_BASE_URL}/modifObjet`, {
id: object.id,
idUser:user.id,
description,
type,
location,
status,
shouldUpdatePoints:true
})
.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">
{t('components.modifObject.title')}
</h1>
</div>
<div className="mb-5">
<label
htmlFor="description"
className="block mb-2 text-sm font-medium text-gray-900"
>
{t('components.modifObject.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"
>
{t('components.modifObject.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"
>
{t('components.modifObject.location')}
</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">
{t('components.modifObject.status')}
</label>
<div className="inline-flex items-center gap-2">
<label
htmlFor="switch-component-on"
className="text-slate-600 text-sm cursor-pointer"
>
{t('components.modifObject.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"
>
{t('components.modifObject.active')}
</label>
</div>
</div>
<div className="mb-5 flex flex-col">
<button
type="submit"
className="text-blue-500 hover:cursor-pointer hover:underline"
>
{t('components.modifObject.confirmMods')}
</button>
<button
type="button"
className="text-red-500 hover:cursor-pointer hover:underline"
onClick={handleCancel}
>
{t('components.modifObject.cancelMods')}
</button>
</div>
</form>
);
}
export default ModifObject;

View File

@ -0,0 +1,170 @@
import React, { useEffect, useState } from "react";
import BoutonGraphique from "./BoutonGraphique";
import { Bell } from "lucide-react";
import Slider from "@mui/material/Slider";
import { API_BASE_URL } from "../config";
import axios from "axios";
import { useAuth } from "../AuthContext";
import { useTranslation } from "react-i18next";
const identifiant = new URLSearchParams(window.location.search).get("id");
function ParticularMeteo({
type,
data,
Icon,
texte1,
texte2,
graphStates,
setGraphStates,
graphRefs,
}) {
const { t } = useTranslation();
const {user} = useAuth();
const [affRegles, setAffRegles] = useState(false);
const [rangeValue, setRangeValue] = useState([0, 0]);
const [alerteActive, setAlerteActive] = useState(false);
const minMaxValues = {
temperature: [-50, 60],
pressure: [940, 1060],
humidity: [10, 100],
};
const MIN = minMaxValues[type][0];
const MAX = minMaxValues[type][1];
const formatLabel = (value) => {
switch (type) {
case "temperature":
return `${value}°C`;
case "pressure":
return `${value} hPa`;
case "humidity":
return `${value}%`;
default:
return value;
}
};
const marks = [
{
value: MIN,
label: formatLabel(MIN),
},
{
value: MAX,
label: formatLabel(MAX),
},
];
useEffect(() => {
axios.get(`${API_BASE_URL}/getRange?id=${identifiant}`).then((response) => {
setRangeValue([
response.data[0][type + "_min"],
response.data[0][type + "_max"],
]);
});
}, [identifiant]);
const color =
rangeValue[0] > data[type] || rangeValue[1] < data[type]
? "text-red-600"
: "text-indigo-600";
const defRangeData = () => {
console.log("Données envoyées :", {
id: identifiant,
min: rangeValue[0],
max: rangeValue[1],
type,
});
axios
.post(`${API_BASE_URL}/modifRangeData`, {
id: identifiant,
idUser:user.id,
min: parseFloat(rangeValue[0]),
max: parseFloat(rangeValue[1]),
type,
})
.then((response) => {
console.log("Modification réussie :", response.data);
})
.catch((error) => {
console.error("Erreur lors de la modification :", error);
});
window.location.reload();
};
const handleChange = (event, newValue) => {
setRangeValue(newValue);
};
function valuetext(value) {
return `${value}°C`;
}
if (data[type]) {
return (
<div className="bg-indigo-50 flex flex-col rounded-lg 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">
<Icon className={`${color}`} size={40} />
</div>
<div className="flex flex-col items-start">
<h1 className={`${color} text-xl font-bold `}>{texte1}</h1>
<h1 className={`${color} text-4xl font-bold`}>
{Math.round(data[type])}{" "}
<span className="text-lg">{texte2}</span>
</h1>
</div>
</div>
<div className="flex gap-2">
{user?.role!=="user" && (
<button
onClick={() => {
setAffRegles(!affRegles);
}}
>
<Bell
className={`${color} hover:pointer-events-auto`}
size={30}
/>
</button>
)}
<BoutonGraphique
type={type}
graphStates={graphStates}
setGraphStates={setGraphStates}
graphCible={graphRefs[type]}
/>
</div>
</div>
{affRegles && (
<div className="p-6">
<h1 className="text-red-500 text-l font-semibold">
{t('components.particularMeteo.defineLimit')}
</h1>
<div className="p-4">
<Slider
getAriaLabel={() => "Temperature range"}
value={rangeValue}
onChange={handleChange}
valueLabelDisplay="auto"
min={MIN}
max={MAX}
marks={marks}
getAriaValueText={valuetext}
disableSwap
/>
</div>
{color=="text-red-600" &&(
<p className="text-red-500 text-l mb-2">{t('components.particularMeteo.outOfBounds')}</p>
)}
<button
type="button"
onClick={() => defRangeData()}
className="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-500 dark:focus:ring-blue-800"
>
{t('components.particularMeteo.setAlert')}
</button>
</div>
)}
</div>
);
}
}
export default ParticularMeteo;

View File

@ -0,0 +1,51 @@
import React,{useEffect, useState} from "react";
import { User } from "lucide-react";
import axios from "axios";
import { API_BASE_URL } from "../config";
import { useTranslation } from "react-i18next";
function UserInfosObject({ user}) {
const { t } = useTranslation();
const [userInfo,setuserInfo]=useState({});
useEffect(()=>{
console.log(user);
axios
.post(`${API_BASE_URL}/publicUser`, {
id: user,
})
.then((response) => {
setuserInfo(response.data);
console.log("Modification réussie :", response.data);
})
.catch((error) => {
console.error("Erreur lors de la modification :", error);
});
},[user]);
return (
<div className="bg-white p-6 rounded-xl min-w-5xl">
<div className="flex align-items gap-6 mb-6">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-1">
<User className="text-indigo-600" size={24} />
</div>
<h1 className="text-black text-2xl font-bold mb-1 ">{t('components.userInfosObject.title')}</h1>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.userInfosObject.pseudo')}</p>
<p className="text-gray-600 capitalize">{userInfo.pseudo}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.userInfosObject.gender')}</p>
<p className="text-gray-600 capitalize">{userInfo.gender}</p>
</div>
<div className="mb-5">
<p className="text-black-900 font-bold">{t('components.userInfosObject.points')}</p>
<p className="text-gray-600">{userInfo.points}</p>
</div>
</div>
);
}
export default UserInfosObject;

View File

@ -0,0 +1,69 @@
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";
import { useTranslation } from "react-i18next";
function WindGraph({ object,reference }) {
const { t } = useTranslation();
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>{t('components.windGraph.date')}</strong> {timestamp}</p>
<p><strong>{t('components.windGraph.windSpeed')}</strong> {wind_speed} km/h</p>
<p><strong>{t('components.windGraph.windDirection')}</strong> {wind_direction}</p>
</div>
);
}
return null;
};
useEffect(() => {
if (reference?.current) {
reference.current.scrollIntoView({ behavior: "smooth" });
}
}, [reference]);
return (
<div key={object.id} ref={reference} 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 ">{t('components.windGraph.title')}</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,76 @@
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";
import { useTranslation } from "react-i18next";
function WindInfo({ object, setGraphStates, graphStates, graphRefs, reference}) {
const { t } = useTranslation();
const [rawData, setRawData] = useState([]);
const identifiant = object.id;
useEffect(() => {
axios.get(`${API_BASE_URL}/wind?id=${identifiant}`).then((response) => {
setRawData(response.data);
});
}, [object]);
useEffect(() => {
if (reference?.current) {
reference.current.scrollIntoView({ behavior: "smooth" });
}
}, [reference]);
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">{t('components.windInfo.currentWind')}</h1>
</div>
{lastData ? (
<div className="flex flex-col items-center gap-1">
<img
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 ">{t('components.windInfo.value')}</h1>
<h1 className="text-gray-700 text-4xl font-bold">
{lastData.wind_speed} <span className="text-lg">Km/h</span>
</h1>
</div>
</div>
<BoutonGraphique
type="wind"
graphStates={graphStates}
setGraphStates={setGraphStates}
graphCible={graphRefs.wind}
/>
</div>
</div>
<h1 className="text-gray-500 text-sm">
{t('components.windInfo.lastRecord')} {lastData.timestamp}
</h1>
</div>
) : (
<p>{t('components.windInfo.loading')}</p>
)}
</div>
);
}
export default WindInfo;

View File

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

24
z1/Front-end/src/i18n.js Normal file
View File

@ -0,0 +1,24 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import frTranslation from './locales/fr.json';
import enTranslation from './locales/en.json';
const resources = {
fr: { translation: frTranslation },
en: { translation: enTranslation }
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'fr',
interpolation: {
escapeValue: false
}
});
export default i18n;

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,363 @@
{
"header": {
"home": "Home",
"about": "About",
"login": "Log in",
"signup": "Sign up",
"visualizer": "Visualizer",
"manage": "Manage",
"dashboard": "Dashboard",
"manageUsers": "Manage Users",
"manageObjects": "Manage Connected Objects",
"profile": "Profile",
"logout": "Log out",
"admin": "Admin"
},
"home": {
"title": "Real-time weather",
"subtitle": "Accurate and personalized forecasts to help you plan your day with peace of mind.",
"searchPlaceholder": "Search for a city ...",
"noCityFound": "No city found.",
"partlyCloudy": "Partly cloudy",
"feelsLike": "Feels like",
"humidity": "Humidity",
"wind": "Wind",
"rain": "Precipitation",
"sunrise": "Sunrise",
"sunset": "Sunset",
"hourlyForecast": "Hourly Forecast",
"dailyForecast": "5-day Forecast",
"weatherServices": "Weather Services",
"viewObjectsTitle": "Visualization of connected weather devices",
"viewObjectsDesc": "Browse all connected devices and their data throughout France",
"viewObjectsBtn": "View objects",
"signupTitle": "Sign up!",
"signupDesc": "Having an account on our site allows you to consult all connected weather objects throughout France.",
"signupBtn": "Sign up",
"loginTitle": "Log in!",
"loginDesc": "Happy to see you again! Log back into your account just as you left it!",
"loginBtn": "Log in",
"consultObjectsTitle": "Consult connected weather devices",
"consultObjectsDesc": "Access real-time data from connected weather devices, modify their parameters and check the history of measurements.",
"consultObjectsBtn": "Explore objects",
"addObjTitle": "Add a new connected device",
"addObjDesc": "Easily integrate a new connected device by entering its information and configuring its parameters for optimal management.",
"addObjBtn": "Add a device",
"adminDashTitle": "Access the Administration dashboard",
"adminDashDesc": "You will be able to manage the site's users as well as the connected devices",
"adminDashBtn": "Administration Dashboard",
"manageObjTitle": "Management of connected devices",
"manageObjDesc": "This module allows you to easily and effectively manage sensors and connected stations in France.",
"manageObjBtn": "Device Management",
"footerRights": "© 2025 VigiMétéo. All rights reserved."
},
"auth": {
"login": {
"title": "Log in",
"emailLabel": "Email:",
"passwordLabel": "Password:",
"submitButton": "Log in",
"noAccount": "Don't have an account?",
"signupLink": "Sign up here",
"loading": "Logging in...",
"success": "Login successful! Redirecting...",
"missingToken": "Authentication failed: missing token in response",
"incorrectAuth": "Incorrect email or password",
"invalidData": "Invalid form data",
"serverError": "Server error. Please try again later.",
"networkError": "Cannot reach the server. Check your internet connection.",
"genericError": "An error occurred. Please try again."
},
"signup": {
"title": "Sign up",
"firstNameLabel": "First Name:",
"lastNameLabel": "Last Name:",
"pseudoLabel": "Username:",
"genderLabel": "Gender:",
"genderMale": "Male",
"genderFemale": "Female",
"emailLabel": "Email:",
"passwordLabel": "Password:",
"confirmPasswordLabel": "Confirm Password:",
"submitButton": "Sign up",
"hasAccount": "Already have an account?",
"loginLink": "Log in here",
"passNoMatch": "Passwords do not match!",
"success": "Sign up successful!",
"error": "Error during sign up"
}
},
"profile": {
"title": "My Profile",
"personalInfo": "Personal Information",
"loyaltyPoints": "Loyalty points:",
"firstName": "First Name:",
"lastName": "Last Name:",
"pseudo": "Username:",
"email": "Email:",
"save": "Save",
"firstNameL": "First Name",
"lastNameL": "Last Name",
"pseudoL": "Username",
"emailL": "Email",
"changePasswordTitle": "Change Password",
"currentPassword": "Current password:",
"newPassword": "New password:",
"confirmNewPassword": "Confirm new password:",
"errorMismatch": "The new passwords do not match!",
"successPass": "Password successfully changed!",
"successUpdate": "Profile successfully updated!",
"errorGeneric": "An error occurred"
},
"about": {
"missionTitle": "Our Mission",
"missionDesc": "Our mission is to provide a complete and innovative solution for climate and environmental monitoring of the French territory. By combining high-quality precise weather forecasts with efficient IoT device management, we aim to offer a centralized platform to monitor local weather conditions in real time, while facilitating the analysis of data collected by IoT devices deployed across the country.",
"whoTitle": "Who are we?",
"whoDesc": "We are a team of tech, innovation, and environmental enthusiasts. We firmly believe that combining real-time weather data and the Internet of Things (IoT) can have a major impact on territorial management. Whether for local authorities, businesses, or public actors, our platform offers the tools needed for proactive and responsive environmental management.",
"visionTitle": "Our Vision",
"visionDesc": "In a world where climate conditions change rapidly, it is essential to anticipate and react effectively to meteorological phenomena. Thanks to our connected devices and intuitive interface, we allow users to track conditions in real time and act accordingly. From climate risk management to urban planning, our platform helps decision-makers make informed choices based on reliable, local data.",
"objectivesTitle": "The Objectives of Our Platform",
"obj1Desc": "Thanks to our connected devices, we collect local weather data, allowing continuous monitoring of climate conditions across the French territory.",
"obj1Title": "Real-time Monitoring",
"obj2Desc": "Using the best weather forecasting technologies, we provide you with accurate forecasts, whether it's temperature, wind speed, or air quality.",
"obj2Title": "Reliable Prediction",
"obj3Desc": "We allow users to easily manage their connected devices (weather stations, sensors, etc.) through a simple interface, while providing real-time tracking of their status and data.",
"obj3Title": "IoT Device Management",
"obj4Desc": "Our platform sends you instant alerts regarding extreme weather phenomena, allowing you to make quick and appropriate decisions.",
"obj4Title": "Rapid Response to Climate Alerts"
},
"admin": {
"sidebar": {
"panel": "Admin Panel",
"dashboard": "Dashboard",
"users": "Users",
"objects": "Device Management"
},
"dashboard": {
"title": "Dashboard",
"manageWidgetsEnd": "Done Managing",
"manageWidgets": "Manage Widgets",
"summary": "Dashboard Summary",
"totalUsers": "Total Users",
"lastLog": "Last Log",
"noLog": "No log available",
"usersList": "User Management",
"username": "Username",
"email": "Email",
"access": "Access",
"noUser": "No users available",
"seeMore": "See more",
"objectsManagement": "Connected Device Management",
"consultObjects": "Consult connected devices",
"addObject": "Add a new device",
"objectsList": "List of Devices and Tools/Services",
"requestDelete": "Device delete request",
"generateReports": "Generate usage reports:",
"requestObjects": "Request devices",
"reportsStats": "Reports and Statistics",
"exportCsv": "Export to CSV",
"energyConsumption": "Total energy consumption",
"energyConsumptionDesc": "1372 kWh accumulated (estimated)",
"connectionRate": "User connection rate",
"connectionRateDesc": "87% of users active this month",
"mostUsedServices": "Most used services",
"addWidget": "Add a widget",
"chooseWidget": "Choose a widget type",
"widgetSummary": "Dashboard Summary",
"widgetUsers": "User Management",
"widgetObjects": "Connected Device Management",
"widgetObjectsList": "List of Devices & Tools",
"widgetReports": "Reports and Statistics",
"widgetDelete": "Device delete request",
"cancel": "Cancel"
},
"user": {
"title": "User Management",
"subtitle": "Add a user from this form",
"firstName": "First Name",
"lastName": "Last Name",
"pseudo": "Username",
"email": "Email",
"password": "Password",
"genderMale": "Male",
"genderFemale": "Female",
"genderOther": "Other",
"addUserBtn": "Add User",
"manageTitle": "Manage users from this panel.",
"gender": "Gender",
"accessLevel": "Access Level",
"points": "Points",
"actions": "Actions",
"changeBtn": "Change",
"deleteBtn": "Delete",
"logsTitle": "Connection history and logs",
"action": "Action",
"timestamp": "Timestamp",
"downloadLogs": "Download Logs",
"successAdd": "User successfully added!",
"errorAdd": "Error adding user!",
"confirmDelete": "Are you sure you want to delete user {name}? This action may result in the deletion of associated objects.",
"successDelete": "User successfully deleted!",
"successLevel": "Access level successfully changed!",
"errorLevel": "There was an error changing the access level!",
"successPoints": "Points successfully saved!",
"errorPoints": "There was an error saving the points!"
},
"adminObjet": {
"title": "Administration of Devices and Tools/Services",
"catsTitle": "Category Management",
"newCatPlaceholder": "New category",
"addBtn": "Add",
"deleteBtn": "Delete",
"listTitle": "List of Devices and Tools/Services",
"sortBy": "-- Sort by --",
"sortOwner": "Owner",
"sortLocation": "Location",
"sortType": "Type",
"sortStatus": "Status",
"colName": "Name",
"colDesc": "Description",
"colType": "Type",
"colLocation": "Location",
"colOwner": "Owner",
"colStatus": "Status",
"noObjects": "No object or service available.",
"requestsTitle": "Device Deletion Requests",
"colObjId": "Device ID",
"colUserId": "User ID",
"colReqDate": "Request Date",
"acceptBtn": "Accept",
"rejectBtn": "Reject",
"noRequests": "No deletion requests found.",
"confirmCatDelete": "Are you sure you want to delete category {cat}? This action might impact devices.",
"successRecord": "Your device has been recorded successfully!",
"successReqDelete": "The request has been successfully deleted!",
"successObjDelete": "Your device has been successfully deleted!",
"errorReqDelete": "There was an error deleting the request!",
"errorObjDelete": "There was an error deleting your device!"
}
},
"gestion": {
"title": "Welcome to the module",
"moduleName": "Management",
"description": "This module allows you to easily and effectively manage connected sensors and stations in France.",
"consultTitle": "Consult connected weather devices",
"consultDesc": "Access real-time data from connected weather devices, modify their parameters and view the history of measurements.",
"exploreBtn": "Explore devices",
"addTitle": "Add a new connected device",
"addDesc": "Easily integrate a new connected device by providing its information and configuring its parameters for optimal management.",
"addBtn": "Add a device",
"objectManagement": {
"successDeleteReq": "Deletion request sent to administrator.",
"errorDeleteReq": "Error, request already sent to administrator.",
"titleAdmin": "Management",
"titleUser": "Visualization",
"titleSuffix": "of",
"titleObjects": "Connected Devices.",
"searchPlaceholder": "Search by name, category, or keywords...",
"filterAll": "All",
"filterStation": "Weather Station",
"filterSensor": "Sensor",
"filterActive": "Active",
"filterInactive": "Inactive",
"noObjects": "No device found",
"moreInfo": "More info",
"deleteItem": "Delete device",
"seeMore": "See more"
},
"objet": {
"dashboardTitle": "Dashboard - ",
"errorFetch": "Error fetching device"
},
"addObject": {
"title": "New device"
}
},
"components": {
"formNewObject": {
"successRecord": "Your device has been successfully registered!",
"errorRecord": "There was an error adding your device!",
"noCategory": "No category available.",
"addTitle": "Add a new device",
"enterData": "Enter the data for your new device",
"confirmData": "Are you sure about this data?",
"name": "Name:",
"description": "Description:",
"type": "Type:",
"selectType": "-- Select a type --",
"location": "Location:",
"batteryType": "Battery type:",
"owner": "Owner:",
"status": "Status:",
"inactive": "Inactive",
"active": "Active",
"confirmInfos": "Confirm information",
"sureBtn": "Yes, I am sure!",
"deleteInfos": "Delete information",
"changeBtn": "No, I want to change!"
},
"infoObject": {
"title": "Information",
"description": "Description:",
"type": "Type:",
"location": "Location:",
"status": "Status:",
"lastUpdate": "Last update:",
"modify": "Modify this information"
},
"modifObject": {
"title": "Modify information",
"description": "Description:",
"type": "Type:",
"location": "Location:",
"status": "Status:",
"inactive": "Inactive",
"active": "Active",
"confirmMods": "Confirm modifications",
"cancelMods": "Cancel modifications"
},
"particularMeteo": {
"defineLimit": "Set the threshold value for the alert:",
"outOfBounds": "Warning, the current value is out of the bounds you defined!",
"setAlert": "Set alert"
},
"meteoInfos": {
"alertInactive": "This device might be inactive due to lack of data. You can make it inactive by changing its status.",
"currentWeather": "Current Weather",
"temperature": "Temperature",
"pressure": "Pressure",
"humidity": "Humidity",
"lastRecord": "Last record:",
"loading": "Loading data..."
},
"batterieInfo": {
"title": "Battery Status",
"batteryType": "Battery type:"
},
"userInfosObject": {
"title": "Owner",
"pseudo": "Username:",
"gender": "Gender:",
"points": "Number of points:"
},
"windGraph": {
"title": "Wind History",
"date": "Date:",
"windSpeed": "Wind speed:",
"windDirection": "Wind direction:"
},
"meteoGraph": {
"historyTemp": "Temperature History",
"historyHum": "Humidity History",
"historyPres": "Pressure History",
"average": "Average"
},
"windInfo": {
"currentWind": "Current Wind",
"value": "Value",
"lastRecord": "Last record:",
"loading": "Loading data..."
}
}
}

View File

@ -0,0 +1,363 @@
{
"header": {
"home": "Accueil",
"about": "À propos",
"login": "Connexion",
"signup": "Inscription",
"visualizer": "Visualisation",
"manage": "Gestion",
"dashboard": "Dashboard",
"manageUsers": "Gestion des Utilisateurs",
"manageObjects": "Gestion des Objets Connectés",
"profile": "Profil",
"logout": "Déconnexion",
"admin": "Admin"
},
"home": {
"title": "La météo en temps réel",
"subtitle": "Prévisions précises et personnalisées pour vous aider à planifier votre journée en toute sérénité.",
"searchPlaceholder": "Rechercher une ville ...",
"noCityFound": "Aucune ville trouvée.",
"partlyCloudy": "Partiellement nuageux",
"feelsLike": "Ressenti",
"humidity": "Humidité",
"wind": "Vent",
"rain": "Précipitations",
"sunrise": "Lever du soleil",
"sunset": "Coucher du soleil",
"hourlyForecast": "Prévisions horaires",
"dailyForecast": "Prévisions 5 jours",
"weatherServices": "Services météo",
"viewObjectsTitle": "Visualisation des objets connectés météorologiques",
"viewObjectsDesc": "Consultez l'ensemble des objets connectés ainsi que leurs données dans l'ensemble de la France",
"viewObjectsBtn": "Voir les objets",
"signupTitle": "Inscrivez-vous !",
"signupDesc": "Avoir un compte sur notre site permet de consulter l'intégralité des objets connectés météorologiques dans l'ensemble de la France",
"signupBtn": "S'inscrire",
"loginTitle": "Connectez-vous !",
"loginDesc": "Heureux de vous retrouver ! Retrouvez votre compte tel que vous l'avez laissé !",
"loginBtn": "Se connecter",
"consultObjectsTitle": "Consulter les objets connectés météorologiques",
"consultObjectsDesc": "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.",
"consultObjectsBtn": "Explorer les objets",
"addObjTitle": "Ajouter un nouvel objet connecté",
"addObjDesc": "Intégrez facilement un nouvel objet connecté en renseignant ses informations et en configurant ses paramètres pour une gestion optimale.",
"addObjBtn": "Ajouter un objet",
"adminDashTitle": "Accéder au tableau d'Administration",
"adminDashDesc": "Vous pourrez gérer les utilisateurs du site mais aussi les objets connectés",
"adminDashBtn": "Tableau d'Administration",
"manageObjTitle": "Gestion des objets connectés",
"manageObjDesc": "Ce module vous permet de gérer les capteurs et stations connectés de France de manière simple et efficace.",
"manageObjBtn": "Gestion des objets",
"footerRights": "© 2025 VigiMétéo. Tous droits réservés."
},
"auth": {
"login": {
"title": "Connexion",
"emailLabel": "Email:",
"passwordLabel": "Mot de passe:",
"submitButton": "Se connecter",
"noAccount": "Vous n'avez pas de compte ?",
"signupLink": "Inscrivez-vous ici",
"loading": "Connexion en cours...",
"success": "Connexion réussie! Redirection...",
"missingToken": "Authentification échouée : token manquant dans la réponse",
"incorrectAuth": "Email ou mot de passe incorrect",
"invalidData": "Données de formulaire invalides",
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
"networkError": "Impossible de joindre le serveur. Vérifiez votre connexion internet.",
"genericError": "Une erreur s'est produite. Veuillez réessayer."
},
"signup": {
"title": "Inscription",
"firstNameLabel": "Prénom:",
"lastNameLabel": "Nom:",
"pseudoLabel": "Pseudo:",
"genderLabel": "Sexe:",
"genderMale": "Homme",
"genderFemale": "Femme",
"emailLabel": "Email:",
"passwordLabel": "Mot de passe:",
"confirmPasswordLabel": "Confirmer le mot de passe:",
"submitButton": "S'inscrire",
"hasAccount": "Vous avez déjà un compte ?",
"loginLink": "Connectez-vous ici",
"passNoMatch": "Les mots de passe ne correspondent pas !",
"success": "Inscription réussie !",
"error": "Erreur lors de l'inscription"
}
},
"profile": {
"title": "Mon Profil",
"personalInfo": "Informations Personnelles",
"loyaltyPoints": "Points de fidélité:",
"firstName": "Prénom:",
"lastName": "Nom:",
"pseudo": "Pseudo:",
"email": "Email:",
"save": "Sauvegarder",
"firstNameL": "Prénom",
"lastNameL": "Nom",
"pseudoL": "Pseudo",
"emailL": "Email",
"changePasswordTitle": "Modifier le mot de passe",
"currentPassword": "Mot de passe actuel:",
"newPassword": "Nouveau mot de passe:",
"confirmNewPassword": "Confirmer le nouveau mot de passe:",
"errorMismatch": "Les nouveaux mots de passe ne correspondent pas !",
"successPass": "Mot de passe modifié avec succès !",
"successUpdate": "Profil mis à jour avec succès !",
"errorGeneric": "Une erreur est survenue"
},
"about": {
"missionTitle": "Notre mission",
"missionDesc": "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.",
"whoTitle": "Qui sommes-nous ?",
"whoDesc": "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.",
"visionTitle": "Notre Vision",
"visionDesc": "Dans un monde où 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.",
"objectivesTitle": "Les Objectifs de Notre Plateforme",
"obj1Desc": "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.",
"obj1Title": "Surveillance en temps réel",
"obj2Desc": "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.",
"obj2Title": "Prédiction fiable",
"obj3Desc": "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.",
"obj3Title": "Gestion des objets connectés",
"obj4Desc": "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.",
"obj4Title": "Réponse rapide aux alertes climatiques"
},
"admin": {
"sidebar": {
"panel": "Admin Panel",
"dashboard": "Tableau de bord",
"users": "Utilisateurs",
"objects": "Gestion des objets"
},
"dashboard": {
"title": "Dashboard",
"manageWidgetsEnd": "Terminer la gestion",
"manageWidgets": "Gérer les widgets",
"summary": "Résumé du tableau de bord",
"totalUsers": "Total Utilisateur",
"lastLog": "Dernier Log",
"noLog": "Aucun log",
"usersList": "Gestion des Utilisateurs",
"username": "Username",
"email": "Email",
"access": "Access",
"noUser": "Aucun utilisateur disponible",
"seeMore": "Voir plus",
"objectsManagement": "Gestion des Objets Connectés",
"consultObjects": "Consulter les objets connectés",
"addObject": "Ajouter un nouvel objet",
"objectsList": "Liste des Objets et Outils/Services",
"requestDelete": "Requête suppression objets",
"generateReports": "Générer des rapports d'utilisation :",
"requestObjects": "Requête objets",
"reportsStats": "Rapports et Statistiques",
"exportCsv": "Exporter en CSV",
"energyConsumption": "Consommation énergétique totale",
"energyConsumptionDesc": "1372 kWh cumulés (estimation)",
"connectionRate": "Taux de connexion des utilisateurs",
"connectionRateDesc": "87% des utilisateurs actifs ce mois-ci",
"mostUsedServices": "Services les plus utilisés",
"addWidget": "Ajouter un widget",
"chooseWidget": "Choisir un type de widget",
"widgetSummary": "Dashboard Summary",
"widgetUsers": "Gestion des Utilisateurs",
"widgetObjects": "Gestion des Objets Connectés",
"widgetObjectsList": "Liste des Objets et Outils/Services",
"widgetReports": "Rapports et Statistiques",
"widgetDelete": "Demande de suppression d'objets",
"cancel": "Annuler"
},
"user": {
"title": "Gestion des utilisateurs",
"subtitle": "Ajoutez un utilisateur à partir de ce formulaire",
"firstName": "Prénom",
"lastName": "Nom",
"pseudo": "Pseudo",
"email": "Email",
"password": "Mot de passe",
"genderMale": "Homme",
"genderFemale": "Femme",
"genderOther": "Autre",
"addUserBtn": "Ajouter utilisateur",
"manageTitle": "Gérez les utilisateurs à partir de ce panneau.",
"gender": "Genre",
"accessLevel": "Niveau d'accès",
"points": "Points",
"actions": "Actions",
"changeBtn": "Changer",
"deleteBtn": "Supprimer",
"logsTitle": "Historique des connexions et journal des logs",
"action": "Action",
"timestamp": "Timestamp",
"downloadLogs": "Télécharger les logs",
"successAdd": "Ajout de l'utilisateur réussi !",
"errorAdd": "Erreur lors de l'ajout de l'utilisateur !",
"confirmDelete": "Êtes-vous sûr de vouloir supprimer l'utilisateur {name} ? Cette action pourrait entraîner la suppression des objets associés.",
"successDelete": "L'utilisateur a bien été supprimé !",
"successLevel": "Le changement de niveau a bien été enregistré !",
"errorLevel": "Il y a eu une erreur dans le changement de niveau !",
"successPoints": "Les points ont bien été enregistrés !",
"errorPoints": "Il y a eu une erreur dans l'ajout des points !"
},
"adminObjet": {
"title": "Administration des Objets et Outils/Services",
"catsTitle": "Gestion des Catégories",
"newCatPlaceholder": "Nouvelle catégorie",
"addBtn": "Ajouter",
"deleteBtn": "Supprimer",
"listTitle": "Liste des Objets et Outils/Services",
"sortBy": "-- Trier par --",
"sortOwner": "Propriétaire",
"sortLocation": "Lieux",
"sortType": "Type",
"sortStatus": "Status",
"colName": "Nom",
"colDesc": "Description",
"colType": "Type",
"colLocation": "Localisation",
"colOwner": "Propriétaire",
"colStatus": "Status",
"noObjects": "Aucun objet ou service disponible.",
"requestsTitle": "Demandes de Suppression d'Objets",
"colObjId": "Objet ID",
"colUserId": "Utilisateur ID",
"colReqDate": "Date de Requête",
"acceptBtn": "Accepter",
"rejectBtn": "Refuser",
"noRequests": "Aucune demande de suppression trouvée.",
"confirmCatDelete": "Êtes-vous sûr de vouloir supprimer la catégorie {cat} ? Cette action pourrait impacter des objets.",
"successRecord": "Votre objet a bien été enregistré !",
"successReqDelete": "La demande a bien été supprimée !",
"successObjDelete": "Votre objet a bien été supprimé !",
"errorReqDelete": "Il y a eu une erreur dans la suppression de la demande !",
"errorObjDelete": "Il y a eu une erreur dans la suppression de votre objet !"
}
},
"gestion": {
"title": "Bienvenue dans le module",
"moduleName": "Gestion",
"description": "Ce module vous permet de gérer les capteurs et stations connectés de France de manière simple et efficace.",
"consultTitle": "Consulter les objets connectés météorologiques",
"consultDesc": "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.",
"exploreBtn": "Explorer les objets",
"addTitle": "Ajouter un nouvel objet connecté",
"addDesc": "Intégrez facilement un nouvel objet connecté en renseignant ses informations et en configurant ses paramètres pour une gestion optimale.",
"addBtn": "Ajouter un objet",
"objectManagement": {
"successDeleteReq": "Demande de suppression envoyée à l'administrateur.",
"errorDeleteReq": "Erreur, demande déjà envoyée à l'administrateur.",
"titleAdmin": "Gestion",
"titleUser": "Visualisation",
"titleSuffix": "des",
"titleObjects": "Objets connectés.",
"searchPlaceholder": "Chercher par nom, catégorie ou mot clés...",
"filterAll": "Tout",
"filterStation": "Station météo",
"filterSensor": "Capteur",
"filterActive": "Actif",
"filterInactive": "Inactif",
"noObjects": "Aucun objet trouvé",
"moreInfo": "Plus d'infos",
"deleteItem": "Supprimer l'objet",
"seeMore": "Voir plus"
},
"objet": {
"dashboardTitle": "Tableau de bord - ",
"errorFetch": "Erreur de récupération de l'objet"
},
"addObject": {
"title": "Nouvel objet"
}
},
"components": {
"formNewObject": {
"successRecord": "Votre objet a bien été enregistré !",
"errorRecord": "Il y a eu une erreur dans l'ajout de votre objet !",
"noCategory": "Aucune catégorie disponible.",
"addTitle": "Ajouter un nouvel objet",
"enterData": "Entrez les données de votre nouvel objet",
"confirmData": "Êtes-vous sûr de ces données ?",
"name": "Nom :",
"description": "Description :",
"type": "Type :",
"selectType": "-- Sélectionner un type --",
"location": "Localisation :",
"batteryType": "Type de batterie :",
"owner": "Propriétaire :",
"status": "Status :",
"inactive": "Inactive",
"active": "Active",
"confirmInfos": "Confirmer les informations",
"sureBtn": "Oui je suis sûr !",
"deleteInfos": "Supprimer les informations",
"changeBtn": "Non je veux changer !"
},
"infoObject": {
"title": "Informations",
"description": "Description :",
"type": "Type :",
"location": "Localisation :",
"status": "Status :",
"lastUpdate": "Dernière mise à jour :",
"modify": "Modifier ces infos"
},
"modifObject": {
"title": "Modifier les infos",
"description": "Description :",
"type": "Type :",
"location": "Localisation :",
"status": "Status :",
"inactive": "Inactive",
"active": "Active",
"confirmMods": "Confirmer les modifications",
"cancelMods": "Annuler les modifications"
},
"particularMeteo": {
"defineLimit": "Définissez la valeur seuil pour l'alerte :",
"outOfBounds": "Attention, la valeur actuelle est hors des bornes que vous avez définies !",
"setAlert": "Définir alerte"
},
"meteoInfos": {
"alertInactive": "Cet objet peut être inactif dû à son manque de données. Vous pouvez le rendre inactif en changeant son statut.",
"currentWeather": "Météo actuelle",
"temperature": "Température",
"pressure": "Pression",
"humidity": "Humidité",
"lastRecord": "Dernier enregistrement :",
"loading": "Chargement des données..."
},
"batterieInfo": {
"title": "Etat de la batterie",
"batteryType": "Type de batterie :"
},
"userInfosObject": {
"title": "Propriétaire",
"pseudo": "Pseudo :",
"gender": "Genre :",
"points": "Nombre de points :"
},
"windGraph": {
"title": "Historique du vent",
"date": "Date :",
"windSpeed": "Vitesse du vent :",
"windDirection": "Direction du vent :"
},
"meteoGraph": {
"historyTemp": "Historique de la température",
"historyHum": "Historique de l'humidité",
"historyPres": "Historique de la pression",
"average": "Moyenne"
},
"windInfo": {
"currentWind": "Vent actuel",
"value": "Valeur",
"lastRecord": "Dernier enregistrement :",
"loading": "Chargement des données..."
}
}
}

11
z1/Front-end/src/main.jsx Normal file
View File

@ -0,0 +1,11 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
import './i18n';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,137 @@
import React from "react";
import { useTranslation } from "react-i18next";
function About() {
const { t } = useTranslation();
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="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">
{t('about.missionTitle')}
</h1>
<p className="text-gray-700 leading-relaxed">
{t('about.missionDesc')}
</p>
</div>
<img
className="rounded-lg h-64 w-full object-cover order-2 md:order-2"
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="/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">
{t('about.whoTitle')}
</h1>
<p className="text-gray-700 leading-relaxed">
{t('about.whoDesc')}
</p>
</div>
{/* Section Notre Vision */}
<div className="order-5 md:order-5">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
{t('about.visionTitle')}
</h1>
<p className="text-gray-700 leading-relaxed">
{t('about.visionDesc')}
</p>
</div>
<img
className="rounded-lg h-64 w-full object-cover order-6 md:order-6"
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">
{t('about.objectivesTitle')}
</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="/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">
{t('about.obj1Desc')}
</p>
</div>
<h1 className="text-xl font-bold mt-4 ">
{t('about.obj1Title')}
</h1>
</div>
{/* Objectif 2 */}
<div className="relative group w-full h-80 mb-7">
<img
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">
{t('about.obj2Desc')}
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">{t('about.obj2Title')}</h1>
</div>
{/* Objectif 3 */}
<div className="relative group w-full h-80 mb-7 border-2 rounded-xl">
<img
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">
{t('about.obj3Desc')}
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">
{t('about.obj3Title')}
</h1>
</div>
{/* Objectif 4 */}
<div className="relative group w-full h-80 mb-7">
<img
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">
{t('about.obj4Desc')}
</p>
</div>
<h1 className="text-xl font-bold mt-4 mb-6">
{t('about.obj4Title')}
</h1>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default About;

View File

@ -0,0 +1,370 @@
import React, { useState, useEffect } from "react";
import Sidebar from "./sidebar.jsx";
import axios from "axios";
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../../config";
import AddObject from "../Gestion/AddObject.jsx";
import FormNewObject from "../../components/FormNewObject.jsx";
function AdminObjet() {
const { t } = useTranslation();
const [categories, setCategories] = useState();
const [newCategory, setNewCategory] = useState("");
const [objects, setObjects] = useState([]);
const [deleteRequests, setDeleteRequests] = useState([]);
useEffect(() => {
axios.get(`${API_BASE_URL}/objets`).then((response) => {
setObjects(response.data);
});
}, []);
useEffect(() => {
axios
.get(`${API_BASE_URL}/getDemandeSuppression`)
.then((response) => {
setDeleteRequests(response.data);
})
.catch((error) => {
console.error(
"Erreur lors de la récupération des requêtes de suppression :",
error
);
});
}, []);
const handleAddCategory = () => {
const trimmed = newCategory.trim();
if (trimmed !== "" && !categories.includes(trimmed)) {
axios
.post(`${API_BASE_URL}/addCategories`, { name: trimmed })
.then((response) => {
console.log("Catégorie ajoutée :", response.data);
setCategories([...categories, trimmed]);
setNewCategory("");
window.location.reload();
})
.catch((error) => {
console.error("Erreur lors de l'ajout de la catégorie :", error);
});
}
};
const handleDeleteCategory = (categoryToDelete) => {
const confirmation = window.confirm(
t('admin.adminObjet.confirmCatDelete').replace('{cat}', categoryToDelete)
);
if (confirmation) {
setCategories(categories.filter((cat) => cat !== categoryToDelete));
axios
.post(`${API_BASE_URL}/deleteCategories`, { name: categoryToDelete })
.then((response) => {
console.log("Catégorie supprimée :", response.data);
window.location.reload();
})
.catch((error) => {
console.error(
"Erreur lors de la suppression de la catégorie :",
error
);
});
} else {
console.log("Suppression annulée.");
}
};
useEffect(() => {
axios
.get(`${API_BASE_URL}/getCategories`)
.then((response) => {
if (response.data.length === 0) {
console.warn("Aucune catégorie disponible.");
} else {
setCategories(response.data);
}
})
.catch((error) => {
console.error("Erreur lors de la récupération des catégories :", error);
});
}, []);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [location, setLocation] = useState("");
const [proprietaire, setProprietaire] = useState("");
const [status, setStatus] = useState("active");
const [verif, setVerif] = useState(false);
const [enregistre, setEnregistre] = useState(false);
const [messRequete, setMessRequete] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
if (verif) {
const newObj = {
id: Date.now(),
name: name.trim(),
description: description.trim(),
type: type.trim(),
location: location.trim(),
proprietaire: proprietaire.trim(),
status: status,
};
setObjects([...objects, newObj]);
setMessRequete(t('admin.adminObjet.successRecord'));
setEnregistre(true);
setVerif(false);
resetForm();
window.location.reload();
} else {
setVerif(true);
}
};
const handleReject =(id) => {
axios
.post(`${API_BASE_URL}/reject`, {
id,
})
.then((response) => {
setMessRequete(t('admin.adminObjet.successReqDelete'));
console.log("La demande à été supprimée :", response.data);
window.location.reload();
})
.catch((error) => {
setMessRequete(
t('admin.adminObjet.errorReqDelete')
);
console.error("Erreur lors de la suppression de la demande :", error);
});
}
const handleDeleteObject = (id) => {
axios
.post(`${API_BASE_URL}/deleteObject`, {
id,
})
.then((response) => {
alert(t('admin.adminObjet.successObjDelete'));
console.log("Votre objet à été supprimé :", response.data);
window.location.reload();
})
.catch((error) => {
alert(
t('admin.adminObjet.errorObjDelete')
);
console.error("Erreur lors de la suppression de l'objet :", error);
});
};
const [sortCriteria, setSortCriteria] = useState("");
const sortedObjects = [...objects].sort((a, b) => {
if (!sortCriteria) return 0;
let fieldA = a[sortCriteria] || "";
let fieldB = b[sortCriteria] || "";
return fieldA.localeCompare(fieldB);
});
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 overflow-auto scrollbar-hide">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl font-bold text-gray-900 text-center mb-12">
{t('admin.adminObjet.title')}
</h1>
<section className="bg-white p-6 rounded-xl shadow-md mb-12">
<h2 className="text-2xl font-semibold mb-4">
{t('admin.adminObjet.catsTitle')}
</h2>
<div className="flex items-center mb-4">
<input
type="text"
placeholder={t('admin.adminObjet.newCatPlaceholder')}
value={newCategory}
onChange={(e) => setNewCategory(e.target.value)}
className="flex-1 border border-gray-300 rounded-lg p-2 mr-4"
/>
<button
onClick={handleAddCategory}
className="bg-indigo-600 text-white px-4 py-2 rounded-lg"
>
{t('admin.adminObjet.addBtn')}
</button>
</div>
<ul>
{categories &&
categories.map((cat, index) => (
<li
key={index}
className="flex justify-between items-center border-b border-gray-200 py-2"
>
<span>{cat}</span>
<button
onClick={() => handleDeleteCategory(cat)}
className="text-red-600 hover:underline"
>
{t('admin.adminObjet.deleteBtn')}
</button>
</li>
))}
</ul>
</section>
<FormNewObject isAdmin={true} />
<section className="bg-white p-6 rounded-xl shadow-md mt-12 mb-12">
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-4">
<h2 className="text-2xl font-semibold">
{t('admin.adminObjet.listTitle')}
</h2>
<select
value={sortCriteria}
onChange={(e) => setSortCriteria(e.target.value)}
className="border border-gray-300 rounded-lg p-2 w-full md:w-auto"
>
<option value="">{t('admin.adminObjet.sortBy')}</option>
<option value="proprietaire">{t('admin.adminObjet.sortOwner')}</option>
<option value="location">{t('admin.adminObjet.sortLocation')}</option>
<option value="type">{t('admin.adminObjet.sortType')}</option>
<option value="status">{t('admin.adminObjet.sortStatus')}</option>
</select>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] divide-y divide-gray-200">
<thead>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('admin.adminObjet.colName')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48">
{t('admin.adminObjet.colDesc')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('admin.adminObjet.colType')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('admin.adminObjet.colLocation')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('admin.adminObjet.colOwner')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{t('admin.adminObjet.colStatus')}
</th>
<th className="px-6 py-3"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedObjects.map((obj) => (
<tr key={obj.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<a
href={`/objet?id=${obj.id}`}
className="text-indigo-600 hover:underline"
>
{obj.name}
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 w-48 truncate">
{obj.description}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{obj.type}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{obj.location}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{obj.proprio_id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{obj.status}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleDeleteObject(obj.id)}
className="text-red-600 hover:underline"
>
{t('admin.adminObjet.deleteBtn')}
</button>
</td>
</tr>
))}
{objects.length === 0 && (
<tr>
<td
colSpan="7"
className="px-6 py-4 text-center text-sm text-gray-500"
>
{t('admin.adminObjet.noObjects')}
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
<section className="bg-white p-6 rounded-xl shadow-md mt-12">
<h2 className="text-2xl font-semibold mb-4">
{t('admin.adminObjet.requestsTitle')}
</h2>
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] divide-y divide-gray-200">
<thead>
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('admin.adminObjet.colObjId')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('admin.adminObjet.colUserId')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('admin.adminObjet.colReqDate')}</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('admin.user.actions')}</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{deleteRequests.map((request) => (
<tr key={request.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{request.id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm"><a className="text-indigo-600 hover:underline" href={`/objet?id=${request.object_id}`}>Objet n°{request.object_id}</a></td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{request.requested_by}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{request.request_date}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 flex gap-2">
<button
className="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-sm"
onClick={() => handleDeleteObject(request.object_id)}
>
{t('admin.adminObjet.acceptBtn')}
</button>
<button
className="bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded-md text-sm"
onClick={() => handleReject(request.id)}
>
{t('admin.adminObjet.rejectBtn')}
</button>
</td>
</tr>
))}
{deleteRequests.length === 0 && (
<tr>
<td
colSpan="5"
className="px-6 py-4 text-center text-sm text-gray-500"
>
{t('admin.adminObjet.noRequests')}
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
);
}
export default AdminObjet;

View File

@ -0,0 +1,408 @@
import React, { useState, useEffect } from "react";
import Sidebar from "./sidebar.jsx";
import { RadioTower,Minus, ArrowRight, BadgePlus, Settings } from "lucide-react";
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../../config.js";
import axios from "axios";
const exportCSV = () => {
const headers = ["Catégorie", "Valeur"];
const rows = [
["Consommation énergétique", "1372 kWh"],
["Taux de connexion", "87%"],
["Service", "Consultation des données météo"],
["Service", "Alertes et suivi de consommation"],
["Service", "Ajout d'objets connectés"],
];
const csvContent =
"\uFEFF" +
[headers, ...rows]
.map((row) => row.map((val) => `"${val.replace(/"/g, '""')}"`).join(","))
.join("\n");
const blob = new Blob([csvContent], {
type: "text/csv;charset=utf-8;",
});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.setAttribute("download", "rapport_plateforme.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const initialWidgets = [
{ id: 1, type: "summary" },
{ id: 2, type: "users" },
{ id: 3, type: "reporting" },
{ id: 4, type: "adminobjet" },
{ id: 5, type: "objects" },
];
function Dashboard() {
const { t } = useTranslation();
const [users, setUsers] = useState([]);
const [logs, setLogs] = useState([
{
id: 1,
username: "complexe",
action: "Accès attribué",
timestamp: new Date().toLocaleString(),
},
{
id: 2,
username: "admin",
action: "Accès attribué",
timestamp: new Date().toLocaleString(),
},
]);
useEffect(() => {
axios.get(`${API_BASE_URL}/users`).then((response) => {
setUsers(response.data);
});
axios.get(`${API_BASE_URL}/objets`).then((response) => {
setAdminObjects(response.data);
});
}, []);
const [adminObjects, setAdminObjects] = useState([]);
const [manageMode, setManageMode] = useState(false);
const [widgets, setWidgets] = useState(initialWidgets);
const [showAddWidgetModal, setShowAddWidgetModal] = useState(false);
const handleDeleteWidget = (id) => {
setWidgets(widgets.filter((widget) => widget.id !== id));
};
const openAddWidgetModal = () => {
setShowAddWidgetModal(true);
};
const handleWidgetSelection = (widgetType) => {
const newWidget = { id: Date.now(), type: widgetType };
setWidgets([...widgets, newWidget]);
setShowAddWidgetModal(false);
};
return (
<div className="flex min-h-screen">
<Sidebar />
<main className="flex-1 bg-gradient-to-br from-blue-50 to-indigo-50 p-8 overflow-auto">
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold truncate mr-2">{t('admin.dashboard.title')}</h1>
<button
onClick={() => setManageMode(!manageMode)}
className="flex items-center justify-center rounded-md hover:bg-gray-300"
aria-label={
manageMode ? t('admin.dashboard.manageWidgetsEnd') : t('admin.dashboard.manageWidgets')
}
>
<span className="p-2 sm:bg-gray-200 sm:px-4 sm:py-2 sm:rounded-md flex items-center">
<Settings size={20} className="sm:mr-2" />
<span className="hidden sm:inline">
{manageMode ? t('admin.dashboard.manageWidgetsEnd') : t('admin.dashboard.manageWidgets')}
</span>
</span>
</button>
</div>
<section>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{widgets.map((widget) => (
<div
key={widget.id}
className="relative bg-white p-6 rounded-xl shadow hover:shadow-md"
>
{manageMode && (
<button
onClick={() => handleDeleteWidget(widget.id)}
className="absolute top-2 right-2 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center"
>
<Minus />
</button>
)}
{widget.type === "summary" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.summary')}
</h2>
<div className="mb-4">
<h3 className="text-lg font-medium">{t('admin.dashboard.totalUsers')}</h3>
<p className="text-2xl">{users.length}</p>
</div>
<div>
<h3 className="text-lg font-medium">{t('admin.dashboard.lastLog')}</h3>
{logs.length > 0 ? (
<p>
{logs[logs.length - 1].username} -{" "}
{logs[logs.length - 1].action}
</p>
) : (
<p>{t('admin.dashboard.noLog')}</p>
)}
</div>
</div>
)}
{widget.type === "users" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.usersList')}
</h2>
<div className="overflow-x-auto">
<table className="min-w-[320px] w-full border border-gray-200">
<thead>
<tr>
<th className="px-2 py-1 border border-gray-200 bg-gray-100 text-left">
{t('admin.dashboard.username')}
</th>
<th className="px-2 py-1 border border-gray-200 bg-gray-100 text-left">
{t('admin.dashboard.email')}
</th>
<th className="px-2 py-1 border border-gray-200 bg-gray-100 text-left">
{t('admin.dashboard.access')}
</th>
</tr>
</thead>
<tbody>
{users.slice(0, 5).map((user) => (
<tr key={user.id}>
<td className="px-2 py-1 border border-gray-200">
{user.pseudo}
</td>
<td className="px-2 py-1 border border-gray-200">
{user.email}
</td>
<td className="px-2 py-1 border border-gray-200">
{user.role}
</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td
colSpan="3"
className="px-2 py-1 border border-gray-200 text-center"
>
{t('admin.dashboard.noUser')}
</td>
</tr>
)}
</tbody>
</table>
</div>
<button
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md"
onClick={() => (window.location.href = "/user")}
>
{t('admin.dashboard.seeMore')}
</button>
</div>
)}
{widget.type === "objects" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.objectsManagement')}
</h2>
<div className="mb-4">
<a
href="/gestionObjets"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
<RadioTower size={24} className="mr-2" />
{t('admin.dashboard.consultObjects')}
<ArrowRight size={16} className="ml-2" />
</a>
</div>
<div>
<a
href="/adminobjet"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
<BadgePlus size={24} className="mr-2" />
{t('admin.dashboard.addObject')}
<ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
)}
{widget.type === "adminobjet" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.objectsList')}
</h2>
<ul className="mb-4 space-y-2">
{adminObjects.slice(0, 2).map((obj) => (
<li
key={obj.id}
className="border border-gray-200 p-2 rounded"
>
<p className="font-medium">{obj.name}</p>
<p className="text-sm text-gray-500">{obj.type}</p>
<p className="text-sm text-gray-500">{obj.status}</p>
</li>
))}
</ul>
<button
onClick={() => (window.location.href = "/adminobjet")}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md"
>
{t('admin.dashboard.seeMore')}
</button>
</div>
)}
{widget.type === "requestObject" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.requestDelete')}
</h2>
<div className="mb-4">
<p className="text-gray-700 mb-2">
{t('admin.dashboard.generateReports')}
</p>
<div className="flex gap-4">
<button
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
onClick={() => exportCSV()}
>
{t('admin.dashboard.requestObjects')}
</button>
</div>
</div>
</div>
)}
{widget.type === "reporting" && (
<div>
<h2 className="text-xl font-semibold mb-4">
{t('admin.dashboard.reportsStats')}
</h2>
<div className="mb-4">
<p className="text-gray-700 mb-2">
{t('admin.dashboard.generateReports')}
</p>
<div className="flex gap-4">
<button
className="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
onClick={() => exportCSV()}
>
{t('admin.dashboard.exportCsv')}
</button>
</div>
</div>
<div className="mt-4 space-y-2">
<div>
<h4 className="text-md font-medium">
{t('admin.dashboard.energyConsumption')}
</h4>
<p className="text-gray-600">
{t('admin.dashboard.energyConsumptionDesc')}
</p>
</div>
<div>
<h4 className="text-md font-medium">
{t('admin.dashboard.connectionRate')}
</h4>
<p className="text-gray-600">
{t('admin.dashboard.connectionRateDesc')}
</p>
</div>
<div>
<h4 className="text-md font-medium">
{t('admin.dashboard.mostUsedServices')}
</h4>
<ul className="list-disc ml-6 text-gray-600">
<li>Consultation des données météo</li>
<li>Alertes et suivi de consommation</li>
<li>Ajout d'objets connectés</li>
</ul>
</div>
</div>
</div>
)}
</div>
))}
<div
onClick={openAddWidgetModal}
className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-xl p-6 hover:bg-gray-50 cursor-pointer"
>
<button className="flex items-center">
<span className="text-3xl font-bold mr-2">+</span>
<span>{t('admin.dashboard.addWidget')}</span>
</button>
</div>
</div>
</section>
{showAddWidgetModal && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="bg-white rounded-lg p-6 w-80">
<h3 className="text-xl font-semibold mb-4">
{t('admin.dashboard.chooseWidget')}
</h3>
<div className="flex flex-col gap-4">
<button
onClick={() => handleWidgetSelection("summary")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetSummary')}
</button>
<button
onClick={() => handleWidgetSelection("users")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetUsers')}
</button>
<button
onClick={() => handleWidgetSelection("objects")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetObjects')}
</button>
<button
onClick={() => handleWidgetSelection("adminobjet")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetObjectsList')}
</button>
<button
onClick={() => handleWidgetSelection("reporting")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetReports')}
</button>
<button
onClick={() => handleWidgetSelection("requestObject")}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-left"
>
{t('admin.dashboard.widgetDelete')}
</button>
</div>
<button
onClick={() => setShowAddWidgetModal(false)}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded-md w-full"
>
{t('admin.dashboard.cancel')}
</button>
</div>
</div>
)}
</main>
</div>
);
}
export default Dashboard;

View File

@ -0,0 +1,358 @@
import React, { useState, useEffect } from "react";
import Sidebar from "./sidebar.jsx";
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../../config.js";
import axios from "axios";
const thTd = "p-2 border border-gray-300 text-left";
const th = `${thTd} bg-gray-100`;
function User() {
const { t } = useTranslation();
const [users, setUsers] = useState([]);
const [logs, setLogs] = useState([]);
const [name, setname] = useState("");
const [surname, setSurname] = useState("");
const [pseudo, setPseudo] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [gender, setGender] = useState("Homme");
const [pointsInput, setPointsInput] = useState({});
const handleAddUser = (e) => {
e.preventDefault();
axios
.post(`${API_BASE_URL}/signup`, {
name,
surname,
pseudo,
email,
password,
confirmPassword:password,
gender,
})
.then((response) => {
logAction(name, "Utilisateur ajouté");
alert(t('admin.user.successAdd'));
window.location.reload();
})
.catch((error) => {
alert(t('admin.user.errorAdd'));
});
};
useEffect(() => {
axios.get(`${API_BASE_URL}/users`).then((response) => {
setUsers(response.data);
});
}, []);
const handleDeleteUser = (userId) => {
const user = users.find((u) => u.id === userId);
if (user) {
const confirmation = window.confirm(
t('admin.user.confirmDelete').replace('{name}', user.name)
);
if (confirmation) {
axios
.post(`${API_BASE_URL}/deleteUser`, {
id: userId,
})
.then((response) => {
alert(t('admin.user.successDelete'));
console.log("L'utilisateur a été supprimé :", response.data);
window.location.reload();
})
.catch((error) => {
console.error(
"Erreur lors de la suppression de l'utilisateur :",
error
);
});
logAction(user.name, "Utilisateur supprimé");
setUsers(users.filter((u) => u.id !== userId));
} else {
console.log("Suppression annulée");
}
}
};
const handleChangeAccessLevel = (userId, newLevel) => {
setUsers(
users.map((user) => {
if (user.id === userId && newLevel !== user.role) {
const oldLevel = user.role;
user.role = newLevel;
if (user.role === "user") {
user.points = 0;
} else if (user.role === "complexe") {
user.points = 100;
} else if (user.role === "admin") {
user.points = 200;
}
axios
.post(`${API_BASE_URL}/setUserPoints`, {
id: user.id,
points: user.points,
})
.then((response) => {
alert(t('admin.user.successLevel'));
console.log("Changement de niveau réussit :", response.data);
})
.catch((error) => {
alert(t('admin.user.errorLevel'));
console.error("Erreur lors du changement de niveau :", error);
});
logAction(
user.name,
`Niveau d'accès changé de ${oldLevel} à ${newLevel}`
);
}
return user;
})
);
};
const handleAdjustPoints = (userId) => {
const pointsToAdd = parseInt(pointsInput[userId]) || 0;
setUsers(
users.map((user) => {
if (user.id === userId) {
user.points = pointsToAdd;
axios
.post(`${API_BASE_URL}/setUserPoints`, {
id: user.id,
points: user.points,
})
.then((response) => {
alert(t('admin.user.successPoints'));
console.log("Ajout des points réussi :", response.data);
})
.catch((error) => {
alert(t('admin.user.errorPoints'));
console.error("Erreur lors de l'ajout des points :", error);
});
logAction(user.name, `Points ajustés à ${pointsToAdd}`);
}
return user;
})
);
setPointsInput({ ...pointsInput, [userId]: "" });
};
const logAction = (name, action) => {
const timestamp = new Date().toLocaleString();
setLogs([...logs, { id: Date.now(), name, action, timestamp }]);
};
const downloadLogs = () => {
const logText = logs
.map((log) => `${log.timestamp} - ${log.name} - ${log.action}`)
.join("\n");
const blob = new Blob([logText], { type: "text/plain;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "logs.txt";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="flex min-h-screen">
<Sidebar />
<main className="flex-grow overflow-x-auto p-5">
<section className="mt-5">
<h1 className="text-2xl font-bold text-gray-900 mb-5">
{t('admin.user.title')}
</h1>
<h2 className="text-xl font-bold text-gray-600 mb-2">{t('admin.user.subtitle')}</h2>
<form className="grid gap-4 mb-5" onSubmit={handleAddUser}>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="lg:col-span-1">
<input
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
type="text"
placeholder={t('admin.user.lastName')}
value={name}
onChange={(e) => setname(e.target.value)}
required
/>
</div>
<div className="lg:col-span-1">
<input
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
type="text"
placeholder={t('admin.user.firstName')}
value={surname}
onChange={(e) => setSurname(e.target.value)}
required
/>
</div>
<div className="lg:col-span-1">
<input
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
type="text"
placeholder={t('admin.user.pseudo')}
value={pseudo}
onChange={(e) => setPseudo(e.target.value)}
required
/>
</div>
<div className="lg:col-span-1">
<input
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
type="email"
placeholder={t('admin.user.email')}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="lg:col-span-1">
<input
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
type="password"
placeholder={t('admin.user.password')}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="lg:col-span-1">
<select
className="w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-300 focus:border-blue-500 focus:outline-none"
value={gender}
onChange={(e) => setGender(e.target.value)}
>
<option value="Homme">{t('admin.user.genderMale')}</option>
<option value="Femme">{t('admin.user.genderFemale')}</option>
<option value="Autre">{t('admin.user.genderOther')}</option>
</select>
</div>
</div>
<div className="mt-2">
<button
className="w-full sm:w-auto px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium border-none rounded-md transition-colors"
type="submit"
>
{t('admin.user.addUserBtn')}
</button>
</div>
</form>
</section>
{/*Tableau utilisateur*/}
<div className="w-full overflow-x-auto">
<h2 className="text-xl font-bold text-gray-600 mb-2">{t('admin.user.manageTitle')}</h2>
<table className="w-full table-auto border border-collapse">
<thead>
<tr>
<th className={th}>{t('admin.user.lastName')}</th>
<th className={th}>{t('admin.user.firstName')}</th>
<th className={th}>{t('admin.user.pseudo')}</th>
<th className={th}>{t('admin.user.email')}</th>
<th className={th}>{t('admin.user.gender')}</th>
<th className={th}>{t('admin.user.accessLevel')}</th>
<th className={th}>{t('admin.user.points')}</th>
<th className={th}>{t('admin.user.actions')}</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td className={thTd}>{user.name}</td>
<td className={thTd}>{user.surname}</td>
<td className={thTd}>{user.pseudo}</td>
<td className={thTd}>{user.email}</td>
<td className={thTd}>{user.gender}</td>
<td className={thTd}>
<select
value={user.role}
onChange={(e) =>
handleChangeAccessLevel(user.id, e.target.value)
}
className="p-2 rounded-md"
>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="complexe">Complexe</option>
</select>
</td>
<td className={thTd}>
<input
className="border p-1 w-20 rounded-md"
type="number"
min="0"
value={pointsInput[user.id] || user.points}
onChange={(e) =>
setPointsInput({
...pointsInput,
[user.id]: e.target.value,
})
}
/>
<button
className="ml-2 p-2 bg-green-600 text-white border-none rounded-md"
onClick={() => handleAdjustPoints(user.id)}
>
{t('admin.user.changeBtn')}
</button>
</td>
<td className={thTd}>
<button
className="p-2 bg-red-600 text-white border-none rounded-md"
onClick={() => handleDeleteUser(user.id)}
>
{t('admin.user.deleteBtn')}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<section className="user-logs mt-10">
<h2 className="text-2xl font-bold text-gray-900 mb-5">
{t('admin.user.logsTitle')}
</h2>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr>
<th className={th}>{t('admin.user.lastName')}</th>
<th className={th}>{t('admin.user.action')}</th>
<th className={th}>{t('admin.user.timestamp')}</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr key={log.id}>
<td className={thTd}>{log.name}</td>
<td className={thTd}>{log.action}</td>
<td className={thTd}>{log.timestamp}</td>
</tr>
))}
</tbody>
</table>
</div>
<button
onClick={downloadLogs}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md"
>
{t('admin.user.downloadLogs')}
</button>
</section>
</main>
</div>
);
}
export default User;

View File

@ -0,0 +1,54 @@
import React from "react";
import { Menu, X } from "lucide-react";
import { useTranslation } from "react-i18next";
function Sidebar({ isOpen, toggleSidebar }) {
const { t } = useTranslation();
return (
<aside
className={`
bg-gray-800 text-white p-5 w-64 fixed top-0 left-0 z-40
transform transition-transform duration-200 ease-in-out
${isOpen ? "translate-x-0" : "-translate-x-full"}
md:static md:translate-x-0
`}
>
<div className="flex justify-between items-center md:hidden mb-4">
<h2 className="text-xl font-bold">{t('admin.sidebar.panel')}</h2>
<button onClick={toggleSidebar} className="focus:outline-none">
<X size={24} />
</button>
</div>
<div className="hidden md:block mb-4">
<h2 className="text-xl font-bold">{t('admin.sidebar.panel')}</h2>
</div>
<nav>
<ul className="list-none p-0">
<li className="mb-3">
<a
className="text-white no-underline hover:underline"
href="/dashboard"
>
{t('admin.sidebar.dashboard')}
</a>
</li>
<li className="mb-3">
<a className="text-white no-underline hover:underline" href="/user">
{t('admin.sidebar.users')}
</a>
</li>
<li className="mb-3">
<a
className="text-white no-underline hover:underline"
href="/adminobjet"
>
{t('admin.sidebar.objects')}
</a>
</li>
</ul>
</nav>
</aside>
);
}
export default Sidebar;

View File

@ -0,0 +1,21 @@
import React, { useState } from "react";
import FormNewObject from "../../components/FormNewObject";
import { useTranslation } from "react-i18next";
function AddObject() {
const { t } = useTranslation();
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">
{t('gestion.addObject.title')}
</h2>
</div>
<FormNewObject />
</div>
</div>
);
}
export default AddObject;

View File

@ -0,0 +1,75 @@
import React from "react";
import {
Search,
MapPin,
Calendar,
Bus,
ArrowRight,
LogIn,
UserPlus,
RadioTower,
Binoculars,
BadgePlus,
} from "lucide-react";
import { useAuth } from "../../AuthContext";
import { useTranslation } from "react-i18next";
function Gestion() {
const { t } = useTranslation();
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">
{t('gestion.title')} <b>{t('gestion.moduleName')}</b>.
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
{t('gestion.description')}
</p>
</div>
<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">
{t('gestion.consultTitle')}
</h3>
<p className="text-gray-600 mb-4">
{t('gestion.consultDesc')}
</p>
<a
href="/gestionObjets"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
{t('gestion.exploreBtn')} <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">
<BadgePlus className="text-indigo-600" size={24} />
</div>
<h3 className="text-xl font-semibold mb-2">
{t('gestion.addTitle')}
</h3>
<p className="text-gray-600 mb-4">
{t('gestion.addDesc')}
</p>
<a
href="/ajouterObjet"
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
{t('gestion.addBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
</div>
</div>
);
}
export default Gestion;

View File

@ -0,0 +1,240 @@
import React from "react";
import { Search, ArrowRight, RadioTower, Plus, Trash } from "lucide-react";
import { useEffect, useState } from "react";
import axios from "axios";
import { API_BASE_URL } from "../../config";
import { useAuth } from "../../AuthContext";
import { useTranslation } from "react-i18next";
import Alert from "../../components/Alert";
import AlertInactive from "../../components/AlertInactive";
function ObjectManagement() {
const { t } = useTranslation();
const { user } = useAuth();
const [searchQuery, setSearchQuery] = useState("");
const [activeFilter, setActiveFilter] = useState("");
const [objects, setObjects] = useState([]);
const [nbAffObject, setnbAffObject] = useState(6);
const [affAlert, setAffAlert] = useState(false);
const [messageAlert, setMessageAlert] = useState("");
const [success, setSuccess] = useState(false);
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(`${API_BASE_URL}/objets`).then((response) => {
setObjects(response.data);
});
}, []);
const handleRequestDeletion = async (objectId) => {
console.log("Demande de suppression pour l'objet", objectId);
try {
// Log des données envoyées
console.log("Envoi de la requête:", {
object_id: objectId,
requested_by: user.id,
});
const response = await axios.post(
`${API_BASE_URL}/requestDeleteObject`,
{
object_id: objectId,
requested_by: user.id,
},
{
headers: {
"Content-Type": "application/json", // Pas d'Authorization
},
}
);
console.log("Réponse du serveur:", response.data);
setMessageAlert(t('gestion.objectManagement.successDeleteReq'));
setAffAlert(true);
setSuccess(false);
} catch (error) {
console.error(
"Erreur lors de la requête :",
error.response?.data || error.message
);
setMessageAlert(t('gestion.objectManagement.errorDeleteReq'));
setAffAlert(true);
setSuccess(true);
}
};
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">
{success ? (
<AlertInactive
affAlert={affAlert}
setAffAlert={setAffAlert}
message={messageAlert}
/>
): <Alert
affAlert={affAlert}
setAffAlert={setAffAlert}
message={messageAlert}/>
}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
{user?.role !== "user" ? t('gestion.objectManagement.titleAdmin') : t('gestion.objectManagement.titleUser')} {" "}
{t('gestion.objectManagement.titleSuffix')} <b>{t('gestion.objectManagement.titleObjects')}</b>
</h2>
</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={t('gestion.objectManagement.searchPlaceholder')}
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>
{/* Filtres responsifs - utilisation de flex-wrap et responsive spacing */}
<div className="flex flex-wrap gap-2 mt-4 justify-center">
<button
onClick={() => setActiveFilter("")}
className={`px-4 py-2 rounded-lg mb-2 ${
activeFilter === ""
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
{t('gestion.objectManagement.filterAll')}
</button>
<button
onClick={() => setActiveFilter("Station")}
className={`px-4 py-2 rounded-lg mb-2 ${
activeFilter === "Station"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
{t('gestion.objectManagement.filterStation')}
</button>
<button
onClick={() => setActiveFilter("Capteur")}
className={`px-4 py-2 rounded-lg mb-2 ${
activeFilter === "Capteur"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
{t('gestion.objectManagement.filterSensor')}
</button>
<button
onClick={() => setActiveFilter("Active")}
className={`px-4 py-2 rounded-lg mb-2 ${
activeFilter === "Active"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
{t('gestion.objectManagement.filterActive')}
</button>
<button
onClick={() => setActiveFilter("Inactive")}
className={`px-4 py-2 rounded-lg mb-2 ${
activeFilter === "Inactive"
? "bg-indigo-600 text-white"
: "bg-white text-gray-600"
}`}
>
{t('gestion.objectManagement.filterInactive')}
</button>
</div>
</div>
{/* Grille responsive pour les objets */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
{filteredDATA.length === 0 ? (
<p className="text-center col-span-full">{t('gestion.objectManagement.noObjects')}</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>
<div className="flex items-center justify-between mt-4">
<a
href={`/objet?id=${object.id}`}
className="flex items-center text-indigo-600 hover:text-indigo-700"
>
{t('gestion.objectManagement.moreInfo')} <ArrowRight size={16} className="ml-2" />
</a>
{user?.role !== "user" && (
<button
onClick={() => handleRequestDeletion(object.id)}
className="text-red-500 hover:text-red-700"
title={t('gestion.objectManagement.deleteItem')}
>
<Trash size={20} />
</button>
)}
</div>
</div>
))
)}
</div>
{nbAffObject < filteredDATA.length && (
<div className="flex items-center flex-col mt-6">
<button
onClick={() => {
setnbAffObject((prev) => prev + 6);
}}
className="hover:bg-indigo-50 p-2 rounded-full transition-colors"
>
<Plus size={40} className="text-indigo-600" />
</button>
<label className="text-indigo-600 font-medium">{t('gestion.objectManagement.seeMore')}</label>
</div>
)}
</div>
</div>
);
}
export default ObjectManagement;

View File

@ -0,0 +1,112 @@
import React from "react";
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";
import { useAuth } from "../../AuthContext";
import { useTranslation } from "react-i18next";
import UserInfosObject from "../../components/UserInfosObject";
function Objet() {
const { t } = useTranslation();
const {user} =useAuth();
const identifiant = new URLSearchParams(window.location.search).get("id");
const [object, setObject] = useState({});
const [graphStates, setGraphStates] = useState({
wind: false,
temperature: false,
pressure: false,
humidity: false,
});
const [afficherModif, defafficherModif] = useState(false);
const graphRefs = {
temperature: useRef(null),
pressure: useRef(null),
humidity: useRef(null),
wind: useRef(null),
};
useEffect(() => {
axios
.post(`${API_BASE_URL}/objet`, {
id: identifiant,
userId:user.id,
shouldUpdatePoints:true,
})
.then((response) => {
setObject(response.data[0]);
})
.catch((error) => {
console.error("Erreur lors de la récupération :", error);
});
}, [user]);
return object && object.id ? (
<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">
{t('gestion.objet.dashboardTitle')} {object.name}
</h2>
</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} />
)}
<WindInfo
object={object}
setGraphStates={setGraphStates}
graphStates={graphStates}
graphRefs={graphRefs}
/>
<MeteoInfos
object={object}
graphStates={graphStates}
setGraphStates={setGraphStates}
graphRefs={graphRefs}
/>
<BatterieInfo object={object} />
<UserInfosObject user={object.proprio_id}/>
</div>
{graphStates.wind && <WindGraph object={object} reference={graphRefs.wind} />}
{graphStates.temperature && (
<MeteoGraph
object={object}
categorie={"temperature"}
Logo={Thermometer}
reference={graphRefs.temperature}
/>
)}
{graphStates.pressure && (
<MeteoGraph
object={object}
categorie={"pressure"}
Logo={CircleGauge}
reference={graphRefs.pressure}
/>
)}
{graphStates.humidity && (
<MeteoGraph
object={object}
categorie={"humidity"}
Logo={Droplet}
reference={graphRefs.humidity}
/>
)}
</div>
</div>
) : (
<h1>{t('gestion.objet.errorFetch')}</h1>
);
}
export default Objet;

View File

@ -0,0 +1,903 @@
import React, { useState, useEffect } from "react";
import {
UserPlus,
LogIn,
BadgePlus,
RadioTower,
Search,
Cloud,
CloudRain,
Droplets,
Wind,
Thermometer,
Sun,
Moon,
MapPin,
CalendarClock,
ArrowRight,
BellRing,
LayoutDashboard,
TowerControl,
} from "lucide-react";
import { useAuth } from "../AuthContext";
import axios from "axios";
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../config";
function EnhancedWeatherHome() {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState("");
const [locations, setLocations] = useState([]);
const [infoMeteo, setInfoMeteo] = useState([]);
const [activeFilter, setActiveFilter] = useState("all");
const [currentTime, setCurrentTime] = useState(new Date());
const { user, token } = useAuth();
const [ville, setVille] = useState("Paris, France");
const heure = currentTime.getHours();
const isDayTime = heure > 6 && heure < 20;
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 60000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
axios
.post(`${API_BASE_URL}/getMeteoHome`, {
location: ville,
})
.then((response) => {
if (response.data.length === 0) {
console.warn("Aucun infos disponible.");
} else {
console.log(response.data[0]);
setInfoMeteo(response.data[0]);
}
})
.catch((error) => {
console.error("Erreur lors de la récupération des infos :", error);
});
}, [ville]);
useEffect(() => {
axios
.get(`${API_BASE_URL}/getLocations`)
.then((response) => {
if (response.data.length === 0) {
console.warn("Aucun lieu disponible.");
} else {
console.log(response.data);
setLocations(response.data);
}
})
.catch((error) => {
console.error("Erreur lors de la récupération des catégories :", error);
});
}, []);
const filteredLocations = locations.filter((loc) =>
loc.toLowerCase().includes(searchQuery.toLowerCase())
);
const formatDate = (date) => {
const options = { weekday: "long", day: "numeric", month: "long" };
return date.toLocaleDateString("fr-FR", options);
};
const formatHeure = (h) => {
const heureFormatee = (h % 24).toString().padStart(2, "0");
return `${heureFormatee}:00`;
};
const hourlyForecast = [
{
time: "Maintenant",
temp: "22°",
icon: <Cloud className="text-indigo-500" size={24} />,
},
{
time: formatHeure(heure + 1),
temp: "22°",
icon: <Cloud className="text-indigo-500" size={24} />,
},
{
time: formatHeure(heure + 2),
temp: "22°",
icon: <Sun className="text-yellow-500" size={24} />,
},
{
time: formatHeure(heure + 3),
temp: "21°",
icon: <Sun className="text-yellow-500" size={24} />,
},
{
time: formatHeure(heure + 4),
temp: "20°",
icon: <CloudRain className="text-blue-500" size={24} />,
},
{
time: formatHeure(heure + 5),
temp: "19°",
icon: <CloudRain className="text-blue-500" size={24} />,
},
];
const dailyForecast = [
{
day: "Lun",
temp: "21°/15°",
icon: <Cloud className="text-indigo-400" size={32} />,
},
{
day: "Mar",
temp: "23°/16°",
icon: <Sun className="text-yellow-500" size={32} />,
},
{
day: "Mer",
temp: "24°/17°",
icon: <Sun className="text-yellow-500" size={32} />,
},
{
day: "Jeu",
temp: "22°/16°",
icon: <CloudRain className="text-blue-400" size={32} />,
},
{
day: "Ven",
temp: "20°/14°",
icon: <CloudRain className="text-blue-400" size={32} />,
},
];
return (
<div
className={`min-h-screen ${
isDayTime
? "bg-gradient-to-br from-blue-100 to-sky-200"
: "bg-gradient-to-br from-blue-900 to-indigo-900 text-white"
}`}
>
{" "}
<div className="relative overflow-hidden">
<div className="absolute inset-0 z-0">
<div
className={`absolute inset-0 ${
isDayTime
? "bg-gradient-to-r from-yellow-200/20 to-blue-300/30"
: "bg-gradient-to-r from-blue-900/50 to-purple-900/60"
}`}
></div>
{isDayTime ? (
<>
<div className="absolute top-10 right-10 w-32 h-32 rounded-full bg-yellow-300 blur-xl opacity-70 animate-pulse"></div>
<div className="absolute -top-10 left-1/4 w-16 h-16 rounded-full bg-white blur-md opacity-30"></div>
<div className="absolute top-1/3 left-1/3 w-12 h-12 rounded-full bg-white blur-md opacity-30"></div>
</>
) : (
<>
<div className="absolute top-10 right-14 w-20 h-20 rounded-full bg-gray-200 blur-lg opacity-70"></div>
<div className="absolute top-14 right-10 w-14 h-14 rounded-full bg-gray-900 blur-sm"></div>
</>
)}
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 relative z-10">
<div className="text-center relative">
<h1
className={`text-5xl font-bold mb-6 ${
isDayTime ? "text-gray-900" : "text-white"
} animate-fade-in`}
>
{t('home.title')}
</h1>
<p
className={`text-xl ${
isDayTime ? "text-gray-700" : "text-gray-200"
} max-w-3xl mx-auto mb-10`}
>
{t('home.subtitle')}
</p>
<div className="max-w-3xl mx-auto relative z-10 mb-16 transition-all duration-300 hover:transform hover:-translate-y-1">
<div className="relative">
<Search
className={`absolute left-4 top-1/2 transform -translate-y-1/2 ${
isDayTime ? "text-gray-400" : "text-gray-500"
}`}
size={24}
/>
<input
type="text"
placeholder={t('home.searchPlaceholder')}
className={`w-full pl-12 pr-4 py-4 rounded-xl border ${
isDayTime
? "border-gray-200 bg-white"
: "border-gray-700 bg-gray-800 text-white"
} focus:outline-none focus:ring-2 focus:ring-indigo-500 shadow-lg`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{searchQuery.length > 0 && (
<ul className="mt-4 space-y-2 bg-white dark:bg-gray-100 rounded-xl p-4 shadow-md">
{filteredLocations.length > 0 ? (
filteredLocations.map((loc, idx) => (
<li
key={idx}
className="p-2 rounded text-gray-800 hover:bg-gray-200 dark:hover:bg-gray-300 cursor-pointer"
onClick={() => {
setVille(loc);
setSearchQuery("");
}}
>
{loc}
</li>
))
) : (
<li className="text-gray-900 dark:text-gray-500">
{t('home.noCityFound')}
</li>
)}
</ul>
)}
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 relative z-10">
<div
className={`rounded-2xl shadow-xl mb-10 overflow-hidden transition-all duration-300 hover:shadow-2xl ${
isDayTime ? "bg-white" : "bg-gray-800"
}`}
>
<div className="p-8">
<div className="flex flex-col lg:flex-row justify-between">
<div className="mb-6 lg:mb-0">
<div className="flex items-center">
<MapPin
className={`${
isDayTime ? "text-indigo-600" : "text-indigo-400"
} mr-2`}
size={20}
/>
<h3
className={`text-xl font-medium ${
isDayTime ? "text-gray-700" : "text-gray-200"
}`}
>
{ville}
</h3>
</div>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
} mb-6`}
>
{formatDate(currentTime)}
</p>
<div className="flex items-center">
{isDayTime ? (
<Cloud className="text-indigo-500 mr-6" size={64} />
) : (
<Moon className="text-indigo-300 mr-6" size={64} />
)}
<div>
<h2
className={`text-6xl font-bold ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
{infoMeteo.temperature}°C
</h2>
<p
className={`text-lg ${
isDayTime ? "text-gray-600" : "text-gray-300"
}`}
>
{t('home.partlyCloudy')}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6">
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.feelsLike')}
</p>
<div className="flex items-center mt-1">
<Thermometer
className={`${
isDayTime ? "text-red-500" : "text-red-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
{infoMeteo.temperature - 2}°C
</span>
</div>
</div>
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.humidity')}
</p>
<div className="flex items-center mt-1">
<Droplets
className={`${
isDayTime ? "text-blue-500" : "text-blue-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
{infoMeteo.humidity}%
</span>
</div>
</div>
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.wind')}
</p>
<div className="flex items-center mt-1">
<Wind
className={`${
isDayTime ? "text-blue-500" : "text-blue-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
{infoMeteo.wind_speed} km/h
</span>
</div>
</div>
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.rain')}
</p>
<div className="flex items-center mt-1">
<CloudRain
className={`${
isDayTime ? "text-blue-500" : "text-blue-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
30%
</span>
</div>
</div>
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.sunrise')}
</p>
<div className="flex items-center mt-1">
<Sun
className={`${
isDayTime ? "text-yellow-500" : "text-yellow-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
06:42
</span>
</div>
</div>
<div
className={`rounded-xl p-4 ${
isDayTime ? "bg-blue-50" : "bg-gray-700"
}`}
>
<p
className={`text-sm ${
isDayTime ? "text-gray-500" : "text-gray-400"
}`}
>
{t('home.sunset')}
</p>
<div className="flex items-center mt-1">
<Moon
className={`${
isDayTime ? "text-indigo-500" : "text-indigo-400"
} mr-2`}
size={18}
/>
<span
className={`text-lg font-medium ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
20:51
</span>
</div>
</div>
</div>
</div>
</div>
<div
className={`px-8 py-6 ${isDayTime ? "bg-gray-50" : "bg-gray-900"}`}
>
<h3
className={`text-lg font-semibold mb-4 ${
isDayTime ? "text-gray-800" : "text-gray-200"
}`}
>
{t('home.hourlyForecast')}
</h3>
<div className="flex overflow-x-auto pb-4 gap-6">
{hourlyForecast.map((item, index) => (
<div
key={index}
className={`flex-shrink-0 flex flex-col items-center p-4 rounded-lg ${
isDayTime ? "bg-white" : "bg-gray-800"
} shadow-sm`}
>
<p
className={`text-sm font-medium ${
isDayTime ? "text-gray-600" : "text-gray-300"
}`}
>
{item.time}
</p>
{item.icon}
<p
className={`text-lg font-bold mt-1 ${
isDayTime ? "text-gray-900" : "text-white"
}`}
>
{item.temp}
</p>
</div>
))}
</div>
</div>
</div>
<div
className={`rounded-2xl shadow-xl mb-10 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
}`}
>
<div className="p-8">
<h3
className={`text-xl font-semibold mb-6 ${
isDayTime ? "text-gray-800" : "text-gray-200"
}`}
>
{t('home.dailyForecast')}
</h3>
<div className="space-y-6">
{dailyForecast.map((item, index) => (
<div
key={index}
className={`flex items-center justify-between p-4 rounded-lg ${
isDayTime ? "hover:bg-gray-50" : "hover:bg-gray-700"
} transition-colors`}
>
<div className="flex items-center">
<span
className={`text-lg font-medium w-12 ${
isDayTime ? "text-gray-700" : "text-gray-300"
}`}
>
{item.day}
</span>
{item.icon}
</div>
<span
className={`text-lg font-bold ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{item.temp}
</span>
</div>
))}
</div>
</div>
</div>
<h3
className={`text-2xl font-bold mb-6 ${
isDayTime ? "text-gray-800" : "text-gray-200"
}`}
>
{t('home.weatherServices')}
</h3>
{user?.role === "user" && (
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-blue-400 to-indigo-500"
: "bg-gradient-to-r from-blue-700 to-indigo-800"
} flex items-center justify-center`}
>
<CalendarClock className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.viewObjectsTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.viewObjectsDesc')}
</p>
<a
href="/gestionObjets"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.viewObjectsBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
)}
{!token && (
<div className="grid md:grid-cols-2 gap-8 mb-12">
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-blue-400 to-indigo-500"
: "bg-gradient-to-r from-blue-700 to-indigo-800"
} flex items-center justify-center`}
>
<UserPlus className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.signupTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.signupDesc')}
</p>
<a
href="/signup"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.signupBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-yellow-300 to-orange-400"
: "bg-gradient-to-r from-blue-700 to-indigo-800"
} flex items-center justify-center`}
>
<LogIn className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.loginTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.loginDesc')}
</p>
<a
href="/login"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.loginBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
</div>
)}
{user?.role === "complexe" && (
<div className="grid md:grid-cols-2 gap-8 mb-12">
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-green-400 to-teal-500"
: "bg-gradient-to-r from-green-700 to-teal-800"
} flex items-center justify-center`}
>
<RadioTower className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.consultObjectsTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.consultObjectsDesc')}
</p>
<a
href="/gestionObjets"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.consultObjectsBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-red-400 to-orange-500"
: "bg-gradient-to-r from-red-700 to-orange-800"
} flex items-center justify-center`}
>
<BadgePlus className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.addObjTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.addObjDesc')}
</p>
<a
href="/ajouterObjet"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.addObjBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
</div>
)}
{user?.role === "admin" && (
<div className="grid md:grid-cols-2 gap-8 mb-12">
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-green-400 to-teal-500"
: "bg-gradient-to-r from-green-700 to-teal-800"
} flex items-center justify-center`}
>
<TowerControl className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.adminDashTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.adminDashDesc')}
</p>
<a
href="/dashboard"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.adminDashBtn')}
<ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
<div
className={`rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 overflow-hidden ${
isDayTime ? "bg-white" : "bg-gray-800"
} transform hover:-translate-y-1`}
>
<div
className={`h-32 ${
isDayTime
? "bg-gradient-to-r from-red-400 to-orange-500"
: "bg-gradient-to-r from-red-700 to-orange-800"
} flex items-center justify-center`}
>
<RadioTower className="text-white" size={48} />
</div>
<div className="p-6">
<h3
className={`text-xl font-semibold mb-2 ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
{t('home.manageObjTitle')}
</h3>
<p
className={`${
isDayTime ? "text-gray-600" : "text-gray-300"
} mb-4`}
>
{t('home.manageObjDesc')}
</p>
<a
href="/gestion"
className={`flex items-center ${
isDayTime
? "text-indigo-600 hover:text-indigo-700"
: "text-indigo-400 hover:text-indigo-300"
}`}
>
{t('home.manageObjBtn')} <ArrowRight size={16} className="ml-2" />
</a>
</div>
</div>
</div>
)}
</div>
<footer className={`py-12 ${isDayTime ? "bg-gray-100" : "bg-gray-900"}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center mb-6 md:mb-0">
<Cloud
className={`${
isDayTime ? "text-indigo-600" : "text-indigo-400"
} mr-2`}
size={24}
/>
<span
className={`text-xl font-bold ${
isDayTime ? "text-gray-800" : "text-white"
}`}
>
VigiMétéo
</span>
</div>
<div
className={`text-sm ${
isDayTime ? "text-gray-600" : "text-gray-400"
}`}
>
{t('home.footerRights')}
</div>
</div>
</div>
</footer>
</div>
);
}
export default EnhancedWeatherHome;

View File

@ -0,0 +1,216 @@
import React, { useState } from "react";
import { Mail, Lock, AlertCircle, CheckCircle, Info, X } from "lucide-react";
import { useNavigate, Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import axios from "axios";
import { useAuth } from "../AuthContext";
import { API_BASE_URL } from "../config";
function Login() {
const { t } = useTranslation();
const [formData, setFormData] = useState({
email: "",
password: "",
});
const [alert, setAlert] = useState({
show: false,
type: "", // 'success', 'error', 'info', 'warning'
message: "",
});
const { login } = useAuth();
const navigate = useNavigate();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
if (alert.show) setAlert({ ...alert, show: false });
};
const showAlert = (type, message) => {
setAlert({
show: true,
type,
message,
});
// Auto-hide success and info alerts after 5 seconds
if (type === 'success' || type === 'info') {
setTimeout(() => {
setAlert(prev => ({ ...prev, show: false }));
}, 5000);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setAlert({ show: false, type: "", message: "" });
try {
// Afficher un message de chargement
showAlert("info", t('auth.login.loading'));
const response = await axios.post(`${API_BASE_URL}/login`, formData, {
headers: {
"Content-Type": "application/json",
},
});
const data = response.data;
if (data.token) {
showAlert("success", t('auth.login.success'));
login(data.token);
// Court délai pour montrer le message de succès avant la redirection
setTimeout(() => {
navigate("/");
}, 1000);
} else {
showAlert("error", t('auth.login.missingToken'));
}
} catch (error) {
console.error("Erreur lors de la connexion", error);
if (error.response) {
if (error.response.status === 401) {
showAlert("error", t('auth.login.incorrectAuth'));
} else if (error.response.status === 422) {
showAlert("error", t('auth.login.invalidData'));
} else if (error.response.status >= 500) {
showAlert("error", t('auth.login.serverError'));
} else {
showAlert("error", error.response.data.message || t('auth.login.genericError'));
}
} else if (error.request) {
showAlert("error", t('auth.login.networkError'));
} else {
showAlert("error", t('auth.login.genericError'));
}
}
};
// Configuration des alertes selon le type
const alertConfig = {
success: {
bgColor: "bg-green-50",
borderColor: "border-green-200",
textColor: "text-green-700",
icon: <CheckCircle className="h-5 w-5 mr-2 mt-0.5 flex-shrink-0 text-green-500" />
},
error: {
bgColor: "bg-red-50",
borderColor: "border-red-200",
textColor: "text-red-700",
icon: <AlertCircle className="h-5 w-5 mr-2 mt-0.5 flex-shrink-0 text-red-500" />
},
info: {
bgColor: "bg-blue-50",
borderColor: "border-blue-200",
textColor: "text-blue-700",
icon: <Info className="h-5 w-5 mr-2 mt-0.5 flex-shrink-0 text-blue-500" />
},
warning: {
bgColor: "bg-yellow-50",
borderColor: "border-yellow-200",
textColor: "text-yellow-700",
icon: <AlertCircle className="h-5 w-5 mr-2 mt-0.5 flex-shrink-0 text-yellow-500" />
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="md:w-96 w-full bg-white rounded-lg shadow-md p-6 mx-auto">
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">
{t('auth.login.title')}
</h2>
{/* Système d'alertes */}
{alert.show && (
<div className={`mb-4 p-3 ${alertConfig[alert.type].bgColor} border ${alertConfig[alert.type].borderColor} ${alertConfig[alert.type].textColor} rounded-md flex items-start justify-between`}>
<div className="flex items-start">
{alertConfig[alert.type].icon}
<span className="text-sm">{alert.message}</span>
</div>
<button
onClick={() => setAlert({ ...alert, show: false })}
className="ml-2 p-1 hover:bg-gray-200 rounded-full"
>
<X size={14} />
</button>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('auth.login.emailLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
/>
</div>
</div>
{/* Mot de passe */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.login.passwordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
autoComplete="current-password"
/>
</div>
</div>
{/* Bouton de connexion */}
<div className="pt-4">
<button
type="submit"
className="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{t('auth.login.submitButton')}
</button>
</div>
{/* Lien vers la page d'inscription */}
<div className="mt-4 text-sm text-center">
<p>
{t('auth.login.noAccount')}
<Link
to="/signup"
className="text-indigo-600 hover:text-indigo-700 font-medium ml-1"
>
{t('auth.login.signupLink')}
</Link>
</p>
</div>
</form>
</div>
</div>
);
}
export default Login;

View File

@ -0,0 +1,330 @@
import React, { useState, useEffect } from 'react';
import { Mail, User, Lock, Edit, Save } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../config";
import { useAuth } from "../AuthContext";
import axios from "axios";
function Profil() {
const { t } = useTranslation();
const [userData, setUserData] = useState({});
const { user } = useAuth();
useEffect(() => {
if (user) {
console.log("user.role:", user.id);
}
}, [user]);
const [formData, setFormData] = useState({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
const [editMode, setEditMode] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const navigate = useNavigate();
useEffect(() => {
axios
.post(`${API_BASE_URL}/user`, {
id: user.id,
})
.then((response) => {
setUserData(response.data);
console.log("Infos récupérées :", response.data);
})
.catch((error) => {
console.error("Erreur lors de la récupération :", error);
});
}, [user]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleProfileChange = (e) => {
const { name, value } = e.target;
setUserData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
setSuccessMessage('');
if (formData.newPassword !== formData.confirmPassword) {
setErrorMessage(t('profile.errorMismatch'));
return;
}
try {
axios
.post(`${API_BASE_URL}/changePassword`, {
id: userData.id,
oldPassword: formData.oldPassword,
newPassword: formData.newPassword
})
.then((response) => {
console.log("Modification du mot de passe réussie :", response.data);
setSuccessMessage(t('profile.successPass'));
setFormData({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
})
.catch((error) => {
console.error("Erreur lors de la modification du mot de passe :", error);
setErrorMessage(error.response?.data?.error || t('profile.errorGeneric'));
});
setSuccessMessage(t('profile.successPass'));
setFormData({
oldPassword: '',
newPassword: '',
confirmPassword: ''
});
} catch (error) {
setErrorMessage(error.message || t('profile.errorGeneric'));
}
};
const handleProfileSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
setSuccessMessage('');
axios
.post(`${API_BASE_URL}/updateProfil`, {
id: userData.id,
name: userData.name,
surname: userData.surname,
pseudo:userData.pseudo,
email: userData.email
})
.catch((error) => {
console.error("Erreur lors de la mise à jour du profil :", error);
setErrorMessage(error.response?.data?.error || t('profile.errorGeneric'));
})
.then((response) => {
console.log("Mise à jour du profil réussie :", response.data);
setSuccessMessage(t('profile.successUpdate'));
setEditMode(false);
});
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">{t('profile.title')}</h1>
{errorMessage && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4">
{errorMessage}
</div>
)}
{successMessage && (
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
{successMessage}
</div>
)}
<div className="grid md:grid-cols-2 gap-8">
{/* Informations du profil */}
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-800">{t('profile.personalInfo')}</h2>
<button
onClick={() => setEditMode(!editMode)}
className="text-indigo-600 hover:text-indigo-800"
>
{editMode ? <Save className="h-5 w-5" /> : <Edit className="h-5 w-5" />}
</button>
</div>
<form onSubmit={handleProfileSubmit} className="space-y-4">
<div className="flex items-center space-x-4 mb-4">
<div className="w-20 h-20 bg-indigo-100 rounded-full flex items-center justify-center">
<User className="h-10 w-10 text-indigo-600" />
</div>
<div>
<h3 className="text-lg font-medium">{userData.name} {userData.surname} ({userData.pseudo})</h3>
<p className="text-gray-500">{userData.email}</p>
</div>
</div>
<div className="bg-indigo-50 p-3 rounded-lg mb-4">
<p className="text-sm text-gray-700">{t('profile.loyaltyPoints')} <span className="font-semibold">{userData.points}</span> ({userData.role})</p>
</div>
{editMode ? (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('profile.firstName')}</label>
<input
type="text"
name="name"
value={userData.name}
onChange={handleProfileChange}
className="block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('profile.lastName')}</label>
<input
type="text"
name="surname"
value={userData.surname}
onChange={handleProfileChange}
className="block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('profile.pseudo')}</label>
<input
type="text"
name="pseudo"
value={userData.pseudo}
onChange={handleProfileChange}
className="block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{t('profile.email')}</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
type="email"
name="email"
value={userData.email}
disabled
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{t('profile.save')}
</button>
</>
) : (
<div className="space-y-3">
<div>
<p className="text-sm font-medium text-gray-500">{t('profile.firstNameL')}</p>
<p className="mt-1">{userData.name}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">{t('profile.lastNameL')}</p>
<p className="mt-1">{userData.surname}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">{t('profile.pseudoL')}</p>
<p className="mt-1">{userData.pseudo}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">{t('profile.emailL')}</p>
<p className="mt-1">{userData.email}</p>
</div>
</div>
)}
</form>
</div>
{/* Changement de mot de passe */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('profile.changePasswordTitle')}</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('profile.currentPassword')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="oldPassword"
value={formData.oldPassword}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('profile.newPassword')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="newPassword"
value={formData.newPassword}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('profile.confirmNewPassword')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
/>
</div>
</div>
<button
type="submit"
className="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{t('profile.changePasswordTitle')}
</button>
</form>
</div>
</div>
</div>
</div>
);
}
export default Profil;

View File

@ -0,0 +1,239 @@
import React, { useState } from 'react';
import { Mail, User, Lock } from 'lucide-react';
import { useNavigate, Link} from 'react-router-dom';
import { useTranslation } from "react-i18next";
import { API_BASE_URL } from "../config.js";
function Signup() {
const { t } = useTranslation();
const [formData, setFormData] = useState({
name: '',
surname: '',
pseudo:'',
email: '',
gender: '',
password: '',
confirmPassword: ''
});
const navigate = useNavigate();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
alert(t('auth.signup.passNoMatch'));
return;
}
try {
const response = await fetch(`${API_BASE_URL}/signup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || t('auth.signup.error'));
}
alert(t('auth.signup.success'));
navigate("/");
} catch (error) {
alert(error.message);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full md:w-96 bg-white rounded-lg shadow-md p-6 mx-auto">
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">{t('auth.signup.title')}</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.signup.firstNameLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.signup.lastNameLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
name="surname"
value={formData.surname}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.signup.pseudoLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
name="pseudo"
value={formData.pseudo}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
/>
</div>
</div>
{/* Sexe */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('auth.signup.genderLabel')}
</label>
<div className="flex gap-6 items-center">
<label className="inline-flex items-center">
<input
type="radio"
name="gender"
value="homme"
checked={formData.gender === 'homme'}
onChange={handleChange}
className="form-radio h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
/>
<span className="ml-2">{t('auth.signup.genderMale')}</span>
</label>
<label className="inline-flex items-center">
<input
type="radio"
name="gender"
value="femme"
checked={formData.gender === 'femme'}
onChange={handleChange}
className="form-radio h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
/>
<span className="ml-2">{t('auth.signup.genderFemale')}</span>
</label>
</div>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('auth.signup.emailLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
/>
</div>
</div>
{/* Mot de passe */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.signup.passwordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
/>
</div>
</div>
{/* Confirmer mot de passe */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('auth.signup.confirmPasswordLabel')}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
required
minLength="8"
/>
</div>
</div>
{/* Bouton d'inscription */}
<div className="pt-4">
<button
type="submit"
className="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{t('auth.signup.submitButton')}
</button>
</div>
{/*Si il a déjà un compte*/}
<div className="mt-4 text-sm text-center">
<p>
{t('auth.signup.hasAccount')}
<Link to="/login" className="text-indigo-600 hover:text-indigo-700 font-medium ml-1"> {t('auth.signup.loginLink')}</Link>
</p>
</div>
</form>
</div>
</div>
);
}
export default Signup;

View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

View File

@ -0,0 +1,23 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
hmr: true, // Hot Module Replacement pour éviter les rechargements complets
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'; // Sépare les gros modules pour un meilleur chargement
}
},
},
},
},
optimizeDeps: {
include: ['lucide-react'], // Réintègre Lucide pour éviter les blocages
},
});

52
z1/docker-compose.yaml Normal file
View File

@ -0,0 +1,52 @@
version: '3.8'
services:
db:
image: postgres:17-alpine
container_name: vigimeteo_db
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: admin
POSTGRES_DB: postgres
volumes:
- vigimeteo_data:/var/lib/postgresql/data
- ./sql/init_db.sql:/docker-entrypoint-initdb.d/init.sql #DB initializes automatically on first run
networks:
- vigimeteo_net
backend:
build:
context: ./Back-end
container_name: vigimeteo_backend
restart: always
environment:
DB_HOST: db
DB_PORT: 5432
DB_NAME: postgres
DB_USER: postgres
DB_PASSWORD: admin
ports:
- "8888:8888"
depends_on:
- db
networks:
- vigimeteo_net
frontend:
build:
context: ./Front-end
container_name: vigimeteo_frontend
restart: always
ports:
- "5000:80"
depends_on:
- backend
networks:
- vigimeteo_net
volumes:
vigimeteo_data:
networks:
vigimeteo_net:

4
z1/prepare-app.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Preparing app..."
docker-compose build
echo "App is prepared."

3
z1/remove-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
echo "Removed app."
docker-compose down -v --rmi local

602
z1/sql/init_db.sql Normal file
View File

@ -0,0 +1,602 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 17.4
-- Dumped by pg_dump version 17.4
-- Started on 2025-04-13 23:02:35
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET transaction_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 226 (class 1259 OID 32826)
-- Name: categories; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.categories (
id integer NOT NULL,
name text NOT NULL
);
ALTER TABLE public.categories OWNER TO postgres;
--
-- TOC entry 225 (class 1259 OID 32825)
-- Name: categories_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.categories_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.categories_id_seq OWNER TO postgres;
--
-- TOC entry 4963 (class 0 OID 0)
-- Dependencies: 225
-- Name: categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.categories_id_seq OWNED BY public.categories.id;
--
-- TOC entry 228 (class 1259 OID 32925)
-- Name: deletion_requests; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.deletion_requests (
id integer NOT NULL,
object_id integer NOT NULL,
requested_by integer NOT NULL,
request_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
status character varying(50) DEFAULT 'pending'::character varying
);
ALTER TABLE public.deletion_requests OWNER TO postgres;
--
-- TOC entry 227 (class 1259 OID 32924)
-- Name: deletion_requests_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.deletion_requests_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.deletion_requests_id_seq OWNER TO postgres;
--
-- TOC entry 4964 (class 0 OID 0)
-- Dependencies: 227
-- Name: deletion_requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.deletion_requests_id_seq OWNED BY public.deletion_requests.id;
--
-- TOC entry 222 (class 1259 OID 32768)
-- Name: range_data; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.range_data (
station_id integer NOT NULL,
temperature_min numeric,
temperature_max numeric,
pressure_min numeric,
pressure_max numeric,
humidity_min numeric,
humidity_max numeric
);
ALTER TABLE public.range_data OWNER TO postgres;
--
-- TOC entry 224 (class 1259 OID 32789)
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.users (
id integer NOT NULL,
name character varying(100) NOT NULL,
surname character varying(100) NOT NULL,
email character varying(255) NOT NULL,
gender character varying(10) NOT NULL,
password character varying(255) NOT NULL,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
points integer DEFAULT 0 NOT NULL,
pseudo character varying(100)
);
ALTER TABLE public.users OWNER TO postgres;
--
-- TOC entry 223 (class 1259 OID 32788)
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.users_id_seq OWNER TO postgres;
--
-- TOC entry 4965 (class 0 OID 0)
-- Dependencies: 223
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- TOC entry 221 (class 1259 OID 16479)
-- Name: weather_data; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.weather_data (
id integer NOT NULL,
station_id integer NOT NULL,
temperature numeric(5,2),
humidity numeric(5,2),
pressure numeric(7,2),
wind_speed numeric(5,2),
wind_direction character varying(50),
"timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE public.weather_data OWNER TO postgres;
--
-- TOC entry 220 (class 1259 OID 16478)
-- Name: weather_data_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.weather_data_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.weather_data_id_seq OWNER TO postgres;
--
-- TOC entry 4966 (class 0 OID 0)
-- Dependencies: 220
-- Name: weather_data_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.weather_data_id_seq OWNED BY public.weather_data.id;
--
-- TOC entry 219 (class 1259 OID 16468)
-- Name: weather_objects; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.weather_objects (
id integer NOT NULL,
name character varying(500) NOT NULL,
description text,
type character varying(100) NOT NULL,
location character varying(255),
last_update timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
status character varying(50) DEFAULT 'active'::character varying,
batterie integer DEFAULT 100,
type_batterie character varying(50),
proprio_id integer,
CONSTRAINT weather_objects_batterie_check CHECK (((batterie >= 0) AND (batterie <= 100)))
);
ALTER TABLE public.weather_objects OWNER TO postgres;
--
-- TOC entry 217 (class 1259 OID 16466)
-- Name: weather_objects_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.weather_objects_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.weather_objects_id_seq OWNER TO postgres;
--
-- TOC entry 218 (class 1259 OID 16467)
-- Name: weather_objects_id_seq1; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE public.weather_objects_id_seq1
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.weather_objects_id_seq1 OWNER TO postgres;
--
-- TOC entry 4967 (class 0 OID 0)
-- Dependencies: 218
-- Name: weather_objects_id_seq1; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--
ALTER SEQUENCE public.weather_objects_id_seq1 OWNED BY public.weather_objects.id;
--
-- TOC entry 4776 (class 2604 OID 32829)
-- Name: categories id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.categories_id_seq'::regclass);
--
-- TOC entry 4777 (class 2604 OID 32928)
-- Name: deletion_requests id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.deletion_requests ALTER COLUMN id SET DEFAULT nextval('public.deletion_requests_id_seq'::regclass);
--
-- TOC entry 4773 (class 2604 OID 32792)
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- TOC entry 4771 (class 2604 OID 16482)
-- Name: weather_data id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_data ALTER COLUMN id SET DEFAULT nextval('public.weather_data_id_seq'::regclass);
--
-- TOC entry 4767 (class 2604 OID 16471)
-- Name: weather_objects id; Type: DEFAULT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_objects ALTER COLUMN id SET DEFAULT nextval('public.weather_objects_id_seq1'::regclass);
--
-- TOC entry 4955 (class 0 OID 32826)
-- Dependencies: 226
-- Data for Name: categories; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.categories (id, name) FROM stdin;
1 station
2 capteur
\.
--
-- TOC entry 4957 (class 0 OID 32925)
-- Dependencies: 228
-- Data for Name: deletion_requests; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.deletion_requests (id, object_id, requested_by, request_date, status) FROM stdin;
\.
--
-- TOC entry 4951 (class 0 OID 32768)
-- Dependencies: 222
-- Data for Name: range_data; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.range_data (station_id, temperature_min, temperature_max, pressure_min, pressure_max, humidity_min, humidity_max) FROM stdin;
1 -33 33 980 1040 30 84
3 -15 39 980 1040 30 90
9 -15 49 980 1040 30 90
2 -15 50 980 1040 30 90
4 -15 50 980 1040 30 90
5 -15 50 980 1040 30 90
6 -15 50 980 1040 30 90
7 -15 50 980 1040 30 90
8 -15 50 980 1040 30 90
10 -15 50 980 1040 30 90
\.
--
-- TOC entry 4953 (class 0 OID 32789)
-- Dependencies: 224
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.users (id, name, surname, email, gender, password, created_at, points, pseudo) FROM stdin;
9 complexe complexe complexe@gmail.com homme $2a$12$LC/9EhIC9z/5IF8y/SjFVuDWqeQbkkafhRtytNJ9VWIvx6lCgHDfq 2025-04-12 13:10:50.562087 100 complexe
7 admin admin admin@a.com homme $2a$12$cugJ4JNxHjL.GE0ONZlkVerXRlKGc3jtVNlo9qQrck1Kahgnz6Fj2 2025-04-11 21:08:47.705738 247 admin
10 user user user@gmail.com homme $2a$12$wja3M3Lc254Ooge7mE5hwuzHEP35YbVzMYYH6WXs5sKc2q4fvlBei 2025-04-12 14:18:22.728679 0 user
11 admin super admin.a@gmail.com homme $2a$12$LC/9EhIC9z/5IF8y/SjFVuDWqeQbkkafhRtytNJ9VWIvx6lCgHDfq 2025-04-13 12:00:00.000000 999999 admin
\.
--
-- TOC entry 4950 (class 0 OID 16479)
-- Dependencies: 221
-- Data for Name: weather_data; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.weather_data (id, station_id, temperature, humidity, pressure, wind_speed, wind_direction, "timestamp") FROM stdin;
1 1 21.50 60.20 1013.10 5.40 Nord-Ouest 2025-03-29 18:47:46.685241
2 2 22.30 55.00 1012.50 3.20 Sud-Ouest 2025-03-29 18:47:46.685241
3 3 24.10 50.00 1010.80 6.00 Est 2025-03-29 18:47:46.685241
4 1 19.80 65.40 1014.00 4.50 Ouest 2025-03-29 18:47:46.685241
5 2 20.60 59.30 1013.50 2.80 Nord 2025-03-29 18:47:46.685241
6 1 22.50 60.00 1012.50 12.30 Nord-Ouest 2025-03-29 08:00:00
7 1 23.00 65.00 1013.25 14.00 Ouest 2025-03-29 09:00:00
8 1 24.00 70.00 1014.75 15.20 Nord 2025-03-29 10:00:00
9 2 21.50 55.00 1011.30 11.00 Sud 2025-03-29 08:30:00
10 2 22.00 60.00 1012.80 13.00 Est 2025-03-29 09:30:00
11 2 23.50 63.00 1013.50 14.50 Sud-Est 2025-03-29 10:30:00
12 3 26.00 58.00 1012.90 17.00 Ouest 2025-03-29 11:00:00
13 3 27.00 60.00 1014.00 18.50 Nord-Ouest 2025-03-29 12:00:00
14 3 28.00 62.00 1015.10 16.00 Nord 2025-03-29 13:00:00
15 4 19.50 75.00 1010.00 9.50 Sud-Ouest 2025-03-29 08:00:00
16 4 20.00 80.00 1010.50 10.00 Sud 2025-03-29 09:00:00
17 4 21.50 85.00 1011.00 11.50 Est 2025-03-29 10:00:00
18 5 18.00 90.00 1010.70 8.00 Ouest 2025-03-29 08:30:00
19 5 18.50 92.00 1011.20 7.00 Nord-Ouest 2025-03-29 09:30:00
20 5 19.00 95.00 1011.80 6.50 Nord 2025-03-29 10:30:00
21 6 24.50 65.00 1013.90 13.00 Sud 2025-03-29 11:00:00
22 6 25.00 66.00 1014.20 14.50 Ouest 2025-03-29 12:00:00
23 6 26.50 68.00 1015.50 16.00 Sud-Ouest 2025-03-29 13:00:00
24 7 21.00 60.00 1012.50 11.50 Est 2025-03-29 08:00:00
25 7 22.50 62.00 1013.00 12.00 Nord-Ouest 2025-03-29 09:00:00
26 7 23.00 64.00 1013.75 13.50 Sud-Est 2025-03-29 10:00:00
27 8 25.00 58.00 1012.10 16.50 Nord 2025-03-29 08:30:00
28 8 26.00 60.00 1013.30 17.50 Ouest 2025-03-29 09:30:00
29 8 27.00 62.00 1014.50 18.00 Sud-Ouest 2025-03-29 10:30:00
30 9 22.00 67.00 1011.40 14.00 Est 2025-03-29 11:00:00
31 9 23.00 69.00 1012.60 15.00 Nord-Ouest 2025-03-29 12:00:00
32 9 24.00 72.00 1013.80 16.00 Nord 2025-03-29 13:00:00
33 10 18.00 55.00 1010.20 10.00 Ouest 2025-03-29 08:00:00
34 10 19.00 58.00 1011.00 11.50 Sud-Ouest 2025-03-29 09:00:00
35 10 20.00 60.00 1011.70 12.50 Est 2025-03-29 10:00:00
\.
--
-- TOC entry 4948 (class 0 OID 16468)
-- Dependencies: 219
-- Data for Name: weather_objects; Type: TABLE DATA; Schema: public; Owner: postgres
--
COPY public.weather_objects (id, name, description, type, location, last_update, status, batterie, type_batterie, proprio_id) FROM stdin;
3 Station Marseille Station météo située à Marseille, France. Mesures de température, humidité, pression et vent. station Marseille, France 2025-03-30 17:01:10.631653 inactive 100 solaire 7
4 Capteur Bordeaux Capteur de température et d'humidité à Bordeaux. capteur Bordeaux, France 2025-03-30 17:53:01.42853 active 100 solaire 7
5 Capteur Lille Capteur de pression atmosphérique à Lille. capteur Lille, France 2025-03-31 21:32:04.955306 inactive 100 solaire 7
6 Capteur Nantes Capteur de vent à Nantes. capteur Nantes, France 2025-03-30 20:10:18.547523 active 100 solaire 7
7 Station Toulouse Station météo à Toulouse mesurant la température, l'humidité, la pression et la vitesse du vent. station Toulouse, France 2025-04-02 15:43:34.803703 active 100 solaire 7
10 Capteur Paris Sud Capteur de température et humidité à Paris Sud. capteur Paris, France 2025-04-02 23:09:38.725522 inactive 100 solaire 7
8 Capteur Grenoble Capteur de température à Grenoble. capteur Grenoble, France 2025-04-04 10:40:08.247433 active 100 solaire 7
1 Station Paris Station météo située à Paris, France. Mesures de température, humidité, pression et vent. station Paris, France 2025-04-11 10:40:57.350173 active 100 solaire 7
2 Station Lyon Station météo située à Lyon, France. Mesures de température, humidité, pression et vent. station Lyon, France 2025-04-11 23:08:56.344369 inactive 100 solaire 7
9 Station Nice Station météo située à Nice, France. Elle mesure la température, l'humidité et la pression. station Nice, France 2025-04-13 19:26:43.601141 active 100 solaire 7
\.
--
-- TOC entry 4968 (class 0 OID 0)
-- Dependencies: 225
-- Name: categories_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.categories_id_seq', 9, true);
--
-- TOC entry 4969 (class 0 OID 0)
-- Dependencies: 227
-- Name: deletion_requests_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.deletion_requests_id_seq', 31, true);
--
-- TOC entry 4970 (class 0 OID 0)
-- Dependencies: 223
-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.users_id_seq', 14, true);
--
-- TOC entry 4971 (class 0 OID 0)
-- Dependencies: 220
-- Name: weather_data_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.weather_data_id_seq', 35, true);
--
-- TOC entry 4972 (class 0 OID 0)
-- Dependencies: 217
-- Name: weather_objects_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.weather_objects_id_seq', 1, false);
--
-- TOC entry 4973 (class 0 OID 0)
-- Dependencies: 218
-- Name: weather_objects_id_seq1; Type: SEQUENCE SET; Schema: public; Owner: postgres
--
SELECT pg_catalog.setval('public.weather_objects_id_seq1', 44, true);
--
-- TOC entry 4792 (class 2606 OID 32835)
-- Name: categories categories_name_key; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.categories
ADD CONSTRAINT categories_name_key UNIQUE (name);
--
-- TOC entry 4794 (class 2606 OID 32833)
-- Name: categories categories_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.categories
ADD CONSTRAINT categories_pkey PRIMARY KEY (id);
--
-- TOC entry 4796 (class 2606 OID 32932)
-- Name: deletion_requests deletion_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.deletion_requests
ADD CONSTRAINT deletion_requests_pkey PRIMARY KEY (id);
--
-- TOC entry 4786 (class 2606 OID 32774)
-- Name: range_data station_meteo_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.range_data
ADD CONSTRAINT station_meteo_pkey PRIMARY KEY (station_id);
--
-- TOC entry 4788 (class 2606 OID 32799)
-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_email_key UNIQUE (email);
--
-- TOC entry 4790 (class 2606 OID 32797)
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- TOC entry 4784 (class 2606 OID 16485)
-- Name: weather_data weather_data_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_data
ADD CONSTRAINT weather_data_pkey PRIMARY KEY (id);
--
-- TOC entry 4782 (class 2606 OID 16477)
-- Name: weather_objects weather_objects_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_objects
ADD CONSTRAINT weather_objects_pkey PRIMARY KEY (id);
--
-- TOC entry 4799 (class 2606 OID 32933)
-- Name: deletion_requests deletion_requests_object_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.deletion_requests
ADD CONSTRAINT deletion_requests_object_id_fkey FOREIGN KEY (object_id) REFERENCES public.weather_objects(id) ON DELETE CASCADE;
--
-- TOC entry 4800 (class 2606 OID 32938)
-- Name: deletion_requests deletion_requests_requested_by_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.deletion_requests
ADD CONSTRAINT deletion_requests_requested_by_fkey FOREIGN KEY (requested_by) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- TOC entry 4797 (class 2606 OID 32820)
-- Name: weather_objects fk_proprio; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_objects
ADD CONSTRAINT fk_proprio FOREIGN KEY (proprio_id) REFERENCES public.users(id) ON DELETE CASCADE;
--
-- TOC entry 4798 (class 2606 OID 32836)
-- Name: weather_data weather_data_station_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.weather_data
ADD CONSTRAINT weather_data_station_id_fkey FOREIGN KEY (station_id) REFERENCES public.weather_objects(id) ON DELETE CASCADE;
-- Completed on 2025-04-13 23:02:36
--
-- PostgreSQL database dump complete
--

4
z1/start-app.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Running app ..."
docker-compose up -d
echo "The app is available at http://localhost:5000"

3
z1/stop-app.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
echo "Stopping app..."
docker-compose stop