Add z1 directory and move of the project
11
z1/Back-end/.editorconfig
Normal 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
@ -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
|
||||
BIN
z1/Back-end/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
18
z1/Back-end/.mvn/wrapper/maven-wrapper.properties
vendored
Normal 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
@ -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
@ -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
BIN
z1/Back-end/keystore.jceks.old
Normal file
308
z1/Back-end/mvnw
vendored
Executable 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
@ -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
@ -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>
|
||||
169
z1/Back-end/src/main/java/com/example/starter/AuthHandler.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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")));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
162
z1/Back-end/src/main/java/com/example/starter/QueryObjects.java
Normal 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;
|
||||
}
|
||||
}
|
||||
151
z1/Back-end/src/main/java/com/example/starter/QueryUsers.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
207
z1/Back-end/src/main/java/com/example/starter/SetObjects.java
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
196
z1/Back-end/src/main/java/com/example/starter/SetUser.java
Normal 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;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
@ -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
@ -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;"]
|
||||
27
z1/Front-end/eslint.config.js
Normal 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
@ -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
@ -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
@ -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"
|
||||
}
|
||||
6
z1/Front-end/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
z1/Front-end/public/img/Est.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
z1/Front-end/public/img/Nord-Est.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
z1/Front-end/public/img/Nord-Ouest.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
z1/Front-end/public/img/Nord.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
z1/Front-end/public/img/NotreMission.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
z1/Front-end/public/img/Ouest.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
z1/Front-end/public/img/Sud-Est.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
z1/Front-end/public/img/Sud-Ouest.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
z1/Front-end/public/img/Sud.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
z1/Front-end/public/img/cloud-sun-rain.svg
Normal 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 |
BIN
z1/Front-end/public/img/fr-alert.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
z1/Front-end/public/img/gestioniot.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
z1/Front-end/public/img/iotmeteo.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
z1/Front-end/public/img/precisionfiable.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
z1/Front-end/public/img/snow.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
z1/Front-end/public/img/surveillancemeteo.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
z1/Front-end/public/img/surveillancetempsreel.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
54
z1/Front-end/src/App.jsx
Normal 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;
|
||||
47
z1/Front-end/src/AuthContext.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
20
z1/Front-end/src/ProtectedRoute.jsx
Normal 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;
|
||||
33
z1/Front-end/src/components/Alert.jsx
Normal 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;
|
||||
23
z1/Front-end/src/components/AlertInactive.jsx
Normal 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;
|
||||
29
z1/Front-end/src/components/BatterieInfo.jsx
Normal 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;
|
||||
25
z1/Front-end/src/components/BoutonGraphique.jsx
Normal 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;
|
||||
280
z1/Front-end/src/components/FormNewObject.jsx
Normal 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;
|
||||
230
z1/Front-end/src/components/Header.jsx
Normal 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;
|
||||
52
z1/Front-end/src/components/InfoObject.jsx
Normal 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;
|
||||
27
z1/Front-end/src/components/LanguageSwitcher.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
98
z1/Front-end/src/components/MeteoGraph.jsx
Normal 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;
|
||||
84
z1/Front-end/src/components/MeteoInfos.jsx
Normal 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;
|
||||
164
z1/Front-end/src/components/ModifObject.jsx
Normal 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;
|
||||
170
z1/Front-end/src/components/ParticularMeteo.jsx
Normal 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;
|
||||
51
z1/Front-end/src/components/UserInfosObject.jsx
Normal 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;
|
||||
69
z1/Front-end/src/components/WindGraph.jsx
Normal 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;
|
||||
76
z1/Front-end/src/components/WindInfo.jsx
Normal 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;
|
||||
1
z1/Front-end/src/config.js
Normal file
@ -0,0 +1 @@
|
||||
export const API_BASE_URL = 'http://localhost:8888';
|
||||
24
z1/Front-end/src/i18n.js
Normal 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;
|
||||
3
z1/Front-end/src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
363
z1/Front-end/src/locales/en.json
Normal 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..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
363
z1/Front-end/src/locales/fr.json
Normal 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, d’innovation et d’environnement. Nous croyons fermement que la combinaison de la donnée météorologique en temps réel et de l’Internet 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 l’environnement.",
|
||||
"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 d’agir 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, qu’il s’agisse de la température, de la vitesse du vent ou de la qualité de l’air.",
|
||||
"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
@ -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>
|
||||
);
|
||||
137
z1/Front-end/src/pages/About.jsx
Normal 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;
|
||||
370
z1/Front-end/src/pages/Admin/AdminObjet.jsx
Normal 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;
|
||||
408
z1/Front-end/src/pages/Admin/Dashboard.jsx
Normal 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;
|
||||
358
z1/Front-end/src/pages/Admin/User.jsx
Normal 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;
|
||||
54
z1/Front-end/src/pages/Admin/sidebar.jsx
Normal 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;
|
||||
21
z1/Front-end/src/pages/Gestion/AddObject.jsx
Normal 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;
|
||||
75
z1/Front-end/src/pages/Gestion/Gestion.jsx
Normal 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;
|
||||
240
z1/Front-end/src/pages/Gestion/ObjectManagement.jsx
Normal 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;
|
||||
112
z1/Front-end/src/pages/Gestion/Objet.jsx
Normal 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;
|
||||
903
z1/Front-end/src/pages/Home.jsx
Normal 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;
|
||||
216
z1/Front-end/src/pages/Login.jsx
Normal 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;
|
||||
330
z1/Front-end/src/pages/Profil.jsx
Normal 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;
|
||||
239
z1/Front-end/src/pages/Signup.jsx
Normal 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;
|
||||
8
z1/Front-end/tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
23
z1/Front-end/vite.config.js
Normal 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
@ -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
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo "Preparing app..."
|
||||
docker-compose build
|
||||
echo "App is prepared."
|
||||
3
z1/remove-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
echo "Removed app."
|
||||
docker-compose down -v --rmi local
|
||||
602
z1/sql/init_db.sql
Normal 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
@ -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
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
echo "Stopping app..."
|
||||
docker-compose stop
|
||||