Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
1 result

Target

Select target project
No results found
Select Git revision
  • main
1 result
Show changes

Commits on Source 25

23 files
+ 496
73
Compare changes
  • Side-by-side
  • Inline

Files

+4 −0
Original line number Original line Diff line number Diff line
@@ -208,6 +208,8 @@ deploy-back-staging:
  rules:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: never
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_BRANCH
      when: always
      when: always
  variables:
  variables:
@@ -222,6 +224,8 @@ deploy-front-staging:
  rules:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: never
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_BRANCH
      when: always
      when: always
  variables:
  variables:
+21 −0
Original line number Original line Diff line number Diff line
@@ -2,6 +2,7 @@
==========
==========
# Run the server-side FastAPI app
# Run the server-side FastAPI app
## In development mode
## In development mode
Using the template files, you first need to create backend/.env and backend/cameras.py.
You can start a virtual environment with the following instructions.
You can start a virtual environment with the following instructions.


```sh
```sh
@@ -34,6 +35,8 @@ Run the server, `docker-compose up -d`


# Run the client-side React app in a different terminal window:
# Run the client-side React app in a different terminal window:


Using the template file, you first need to create frontend/.env.

```sh
```sh
$ cd frontend
$ cd frontend
$ npm install
$ npm install
@@ -44,6 +47,18 @@ Navigate to [http://localhost:3000](http://localhost:3000)


<br/>  
<br/>  


# Documentation

## Algorithm

The crowd-counting AI model used is based on this repository:
https://github.com/ZhengPeng7/W-Net-Keras
The dataset used is ShanghaiTech Part B.
The model is given a 3-channel image and generates a density map of half the size of the input image.
The estimated number of people is obtained by summing on all pixels of the density map.

<br/>

# TODO
# TODO
## Coté algo
## Coté algo
- Accéder à d'autre infos telle que l'API des cours sur demande à la DISI pour intégrer ça aux prédictions (ex: cours en promo complète juste implique plus d'attente)
- Accéder à d'autre infos telle que l'API des cours sur demande à la DISI pour intégrer ça aux prédictions (ex: cours en promo complète juste implique plus d'attente)
@@ -67,3 +82,9 @@ Navigate to [http://localhost:3000](http://localhost:3000)
## Documentation
## Documentation
- Documenter le projet au maximum
- Documenter le projet au maximum
- Réfléchir à opensourcer (ça peut permettre d'étendre plus facilement à d'autre RU)
- Réfléchir à opensourcer (ça peut permettre d'étendre plus facilement à d'autre RU)

## Déploiement
- Monitorer la VM de prod avec Datadog
- Écrire un rôle Ansible de déploiement en prod
- Monitorer la VM de staging avec Datadog
- Écrire un rôle Ansible de déploiement en staging

backend/cameras.py

0 → 100644
+43 −0
Original line number Original line Diff line number Diff line
restaurants = [
    {
        "restaurant": "local",
        "a_factor": 30,
        "b_factor": 120,
        "cameras":
            [
                {
                    "IP": "10.148.38.3",
                    "user": "viarezocam",
                    "password": "superponey",
                    "stream": "stream1",
                    "mask_points":
                        [
                            [
                                [70, 370],
                                [420, 720],
                                [1280, 720],
                                [1280, 250],
                                [930, 215],
                                [450, 550],
                                [130, 350]
                            ]
                        ],
                    "checkouts":
                        [
                            {
                                "x1": 380,
                                "x2": 435,
                                "y1": 740,
                                "y2": 780
                            },
                            {
                                "x1": 300,
                                "x2": 350,
                                "y1": 830,
                                "y2": 880
                            }
                        ]
                }
            ]
    }
]
Original line number Original line Diff line number Diff line
restaurants = [
restaurants = [
    {
    {
        "restaurant": "local",
        "restaurant": "local",
        "a_factor": 30,
        # the linear function estimating the waiting time t based on the number of people n and the number c of open checkouts is:
        "b_factor": 120,
        # t = a * n / c + b
        "a_factor": 30, # slope
        "b_factor": 120, # y-intercept
        # list of all the cameras that correspond to a same restaurant
        "cameras":
        "cameras":
            [
            [
                {   
                {   
                    # the RTSP url will be: rtsp://user:password@IP:stream
                    "IP": "",
                    "IP": "",
                    "user": "",
                    "user": "",
                    "password": "",
                    "password": "",
                    "stream":  "stream1",
                    "stream":  "stream1",
                    # list of the coordinates of the points which constitue the region that should be masked on the picture
                    "mask_points": 
                    "mask_points": 
                        [
                        [
                            # polygon which should be part of the mask
                            [
                            [
                                # [x, y] coordinates of each vertex of the polygon (on a 1280*720 picture)
                                [70, 370],
                                [70, 370],
                                [420, 720],
                                [420, 720],
                                [1280, 720],
                                [1280, 720],
@@ -22,8 +29,10 @@ restaurants = [
                                [130, 350]
                                [130, 350]
                            ]
                            ]
                        ],
                        ],
                    "caisses":
                    # list of the coordinates of each checkout
                    "checkouts":
                        [
                        [
                            # each checkout corresponds to a rectangle, indicated by two x and two y coordinates
                            {
                            {
                                "x1": 380,
                                "x1": 380,
                                "x2": 435,
                                "x2": 435,
+40 −25
Original line number Original line Diff line number Diff line
@@ -214,7 +214,7 @@ def delete_comment(id: int, db: Session):


# Define CRUD operation for the news
# Define CRUD operation for the news


def get_news(place: str, db: Session):
def get_news(place: str, admin: bool, db: Session):
    """ Get the news for the given place """
    """ Get the news for the given place """
    current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
    current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
    news = db.query(
    news = db.query(
@@ -222,37 +222,50 @@ def get_news(place: str, db: Session):
        models.News.place == place,
        models.News.place == place,
        models.News.end_date >= current_date).order_by(
        models.News.end_date >= current_date).order_by(
        models.News.published_at.desc()).all()
        models.News.published_at.desc()).all()
    if admin:
        return news

    opening_hours = db.query(
    opening_hours = db.query(
        models.OpeningHours.open_time,
        models.OpeningHours.open_time,
        models.OpeningHours.close_time).filter(
        models.OpeningHours.close_time,
        models.OpeningHours.day).filter(
        models.OpeningHours.place == place,
        models.OpeningHours.place == place,
        models.OpeningHours.day == current_date.weekday()).order_by(
        models.OpeningHours.day >= current_date.weekday()).order_by(
        models.OpeningHours.day,
        models.OpeningHours.open_time).all()
        models.OpeningHours.open_time).all()
    next_timetable = None
    if not opening_hours:
    for time_slot in opening_hours:
        opening_hours = db.query(
        if current_date.time() < time_slot.open_time:
            models.OpeningHours.open_time,
            next_timetable = time_slot.open_time
            models.OpeningHours.close_time,
            models.OpeningHours.day).filter(
            models.OpeningHours.place == place).order_by(
            models.OpeningHours.day,
            models.OpeningHours.open_time).all()
    next_time_slot = None
    for open_time, close_time, day in opening_hours:
        next_date = current_date + timedelta(days=day - current_date.weekday())
        if day < current_date.weekday():
            next_date = next_date + timedelta(days=7)
            next_time_slot = datetime.combine(next_date.date(), open_time)
            break
            break
    if not next_timetable:
        if current_date < pytz.timezone("Europe/Paris").localize(datetime.combine(next_date.date(), close_time)):
        closure = db.query(
            next_time_slot = datetime.combine(next_date.date(), open_time)
            models.Closure).filter(
            break
            models.Closure.place == place,
    if next_time_slot:
            models.Closure.beginning_date <= current_date,
            models.Closure.end_date >= current_date).first()
    else:
        closure = db.query(
        closure = db.query(
            models.Closure).filter(
            models.Closure).filter(
            models.Closure.place == place,
            models.Closure.place == place,
            models.Closure.beginning_date <= datetime.combine(current_date.date(), next_timetable),
            models.Closure.beginning_date <= next_time_slot,
            models.Closure.end_date >= datetime.combine(current_date.date(), next_timetable)).first()
            models.Closure.end_date > next_time_slot).first()
        if closure:
        if closure:
            closure_news = schemas.News(
            closure_news = schemas.News(
                title="Fermeture exceptionnelle",
                title="Fermeture exceptionnelle",
                content=f"{place} est exceptionnellement hors service jusqu'au {closure.end_date.strftime('%d/%m/%y à %Hh%M')}",
                content=f"{place} est exceptionnellement hors service jusqu'au {closure.end_date.strftime('%d/%m/%y à %Hh%M')}",
                end_date=closure.end_date,
                end_date=closure.end_date,
                place=place,
                place=place,
            published_at=closure.beginning_date)
                published_at=current_date)
            news.append(closure_news)
            news.append(closure_news)

    return news
    return news




@@ -382,6 +395,7 @@ def update_user(user: schemas.User, user_info: dict, db: Session):
    if existing_user:
    if existing_user:
        existing_user.cookie = user.cookie
        existing_user.cookie = user.cookie
        existing_user.expiration_date = expiration_date
        existing_user.expiration_date = expiration_date
        existing_user.admin = "admin eatfast" in user_info["roles"]
        db.delete(user)
        db.delete(user)
        db.add(existing_user)
        db.add(existing_user)
        db.commit()
        db.commit()
@@ -390,6 +404,7 @@ def update_user(user: schemas.User, user_info: dict, db: Session):
    else:
    else:
        user.username = full_name
        user.username = full_name
        user.expiration_date = expiration_date
        user.expiration_date = expiration_date
        user.admin = "admin eatfast" in user_info["roles"]
        db.add(user)
        db.add(user)
        db.commit()
        db.commit()
        db.refresh(user)
        db.refresh(user)
@@ -531,7 +546,7 @@ def create_closure(closure: schemas.Closure, db: Session):
    db.add(db_closure)
    db.add(db_closure)
    db.commit()
    db.commit()
    db.refresh(db_closure)
    db.refresh(db_closure)
    return schemas.Closure(**closure.dict())
    return schemas.Closure(**db_closure.__dict__)




def delete_closure(id: int, db: Session):
def delete_closure(id: int, db: Session):
Original line number Original line Diff line number Diff line
"""
"""
Models of the database for magasin app
Models of the database for magasin app
"""
"""
from sqlalchemy import Column, ForeignKey, Integer, DateTime, Float, Interval, String, Text, Time
from sqlalchemy import Boolean, Column, ForeignKey, Integer, DateTime, Float, Interval, String, Text, Time
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship


from db.database import Base
from db.database import Base
@@ -82,5 +82,6 @@ class Users(Base):
    username = Column(String(50))
    username = Column(String(50))
    cookie = Column(String(50))
    cookie = Column(String(50))
    expiration_date = Column(DateTime)
    expiration_date = Column(DateTime)
    admin = Column(Boolean)
    comments = relationship("Comments")
    comments = relationship("Comments")
    comments = relationship("CollaborativeRecords")
    comments = relationship("CollaborativeRecords")
Original line number Original line Diff line number Diff line
@@ -68,7 +68,7 @@ class NewsBase(BaseModel):


class News(NewsBase):
class News(NewsBase):
    """Database news base schema"""
    """Database news base schema"""
    id: int
    id: Optional[int]
    published_at: datetime = Field(..., title="Publication date of the news")
    published_at: datetime = Field(..., title="Publication date of the news")


    class Config:
    class Config:
@@ -138,3 +138,4 @@ class User(BaseModel):
    username: str
    username: str
    cookie: str
    cookie: str
    expiration_date: datetime
    expiration_date: datetime
    admin: Optional[bool] = Field(default=False, title="Set to true to allow access to the admin interface")
+23 −3
Original line number Original line Diff line number Diff line
from fastapi import FastAPI
from fastapi import Cookie, Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from sqlalchemy.orm import Session
from dotenv import load_dotenv
from dotenv import load_dotenv
from threading import Thread
from threading import Thread
from asyncio import run
from asyncio import run
import os
import os


from db import database, models
from db import database, models, crud
from db.database import get_db
from routers import *
from routers import *
from video_capture import handle_cameras
from video_capture import handle_cameras


app = FastAPI(docs_url="/api/docs", openapi_url="/api/openapi.json")
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)


# load environment variables
# load environment variables
load_dotenv()
load_dotenv()
@@ -35,6 +40,21 @@ async def on_startup():
    t.start()
    t.start()




# Docs OpenAPI
@app.get("/api/openapi.json")
async def get_open_api_endpoint(connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    if user.admin:
        return JSONResponse(get_openapi(title="FastAPI", version=1, routes=app.routes))


@app.get("/api/docs")
async def get_documentation(connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    if user.admin:
        return get_swagger_ui_html(openapi_url="/api/openapi.json", title="docs")


# Integration of routers
# Integration of routers
app.include_router(infos.router)
app.include_router(infos.router)
app.include_router(records.router)
app.include_router(records.router)
Original line number Original line Diff line number Diff line
@@ -19,7 +19,7 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.get("/")
@router.get("/")
async def whoami(connect_id: str = Cookie(...), db: Session = Depends(get_db)):
async def whoami(connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    user = crud.get_user(connect_id, db)
    return user.username
    return {"name": user.username, "admin": user.admin}




@router.get("/login")
@router.get("/login")
Original line number Original line Diff line number Diff line
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Cookie, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session
from typing import List
from typing import List


@@ -34,8 +34,12 @@ async def get_closure(place: str, db: Session = Depends(get_db)):




@router.post('/closure', response_model=schemas.Closure)
@router.post('/closure', response_model=schemas.Closure)
async def create_closure(closure: schemas.Closure, db: Session = Depends(get_db)):
async def create_closure(closure: schemas.ClosureBase, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    if user.admin:
        return crud.create_closure(closure, db)
        return crud.create_closure(closure, db)
    else:
        raise HTTPException(status_code=403, detail="Administrator privilege required")




@router.delete('/closure/{id}', response_model=None)
@router.delete('/closure/{id}', response_model=None)
Original line number Original line Diff line number Diff line
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Cookie, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session
from typing import List
from typing import List
import json
import json
@@ -12,17 +12,25 @@ router = APIRouter(prefix="/api", tags=["news"])




@router.get('/{place}/news', response_model=List[schemas.News])
@router.get('/{place}/news', response_model=List[schemas.News])
async def get_news(place: str, db: Session = Depends(get_db)):
async def get_news(place: str, admin: bool = False, db: Session = Depends(get_db)):
    return crud.get_news(place, db)
    return crud.get_news(place, admin, db)




@router.post('/news', response_model=schemas.News)
@router.post('/news', response_model=schemas.News)
async def create_news(news: schemas.NewsBase, db: Session = Depends(get_db)):
async def create_news(news: schemas.NewsBase, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    if user.admin:
        saved_news = crud.create_news(news, db)
        saved_news = crud.create_news(news, db)
        await manager.broadcast(json.dumps({"type": "news", "comment": saved_news.__dict__}, default=str))
        await manager.broadcast(json.dumps({"type": "news", "comment": saved_news.__dict__}, default=str))
        return saved_news
        return saved_news
    else:
        raise HTTPException(status_code=403, detail="Administrator privilege required")




@router.delete('/news/{id}', response_model=None)
@router.delete('/news/{id}', response_model=None)
async def delete_news(id: int, db: Session = Depends(get_db)):
async def delete_news(id: int, connect_id: str = Cookie(...), db: Session = Depends(get_db)):
    user = crud.get_user(connect_id, db)
    if user.admin:
        return crud.delete_news(id, db)
        return crud.delete_news(id, db)
    else:
        raise HTTPException(status_code=403, detail="Administrator privilege required")
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@ async def handle_cameras():
    db = SessionLocal()
    db = SessionLocal()
    for restaurant in restaurants:
    for restaurant in restaurants:
        for camera in restaurant["cameras"]:
        for camera in restaurant["cameras"]:
            # each camera masked is constructed based on the given coordiantes and saved
            mask = np.zeros((720, 1280, 3), dtype=np.float32)
            mask = np.zeros((720, 1280, 3), dtype=np.float32)
            cv2.fillPoly(mask, np.array(camera["mask_points"]), (255, 255, 255))
            cv2.fillPoly(mask, np.array(camera["mask_points"]), (255, 255, 255))
            camera["mask"] = mask
            camera["mask"] = mask
@@ -28,6 +29,7 @@ async def handle_cameras():


        for restaurant in restaurants:
        for restaurant in restaurants:


            # we check whether or not the restaurant is opened
            is_open = db.query(
            is_open = db.query(
                models.OpeningHours).filter(
                models.OpeningHours).filter(
                models.OpeningHours.place == restaurant["restaurant"],
                models.OpeningHours.place == restaurant["restaurant"],
@@ -36,13 +38,15 @@ async def handle_cameras():
                models.OpeningHours.close_time >= current_time).first() is not None
                models.OpeningHours.close_time >= current_time).first() is not None


            if is_open:
            if is_open:
                count_prediction = 0
                count_prediction = 0  # estimated number of people in the restaurant
                open_checkouts = 0
                open_checkouts = 0  # estimated number of open checkouts in the restaurant
                cams_working = True
                cams_working = True  # boolean indicating whether or not all cams are working in the restaurant


                for camera in restaurant["cameras"]:
                for camera in restaurant["cameras"]:
                    # connection to the rtsp stream
                    cap = cv2.VideoCapture(f'rtsp://{camera["user"]}:{camera["password"]}@{camera["IP"]}/{camera["stream"]}')
                    cap = cv2.VideoCapture(f'rtsp://{camera["user"]}:{camera["password"]}@{camera["IP"]}/{camera["stream"]}')
                    if cams_working and cap.isOpened():
                    if cams_working and cap.isOpened():
                        # extraction and preoprocessing of the first frame
                        _, frame = cap.read()
                        _, frame = cap.read()
                        masked_img = cv2.bitwise_and(
                        masked_img = cv2.bitwise_and(
                            frame.astype(np.float32), camera["mask"])
                            frame.astype(np.float32), camera["mask"])
@@ -53,16 +57,19 @@ async def handle_cameras():
                                    np.array(
                                    np.array(
                                        [treated_img]))),
                                        [treated_img]))),
                            axis=0)
                            axis=0)
                        # getting the density map from the model and the number of people
                        pred_map = np.squeeze(model.predict(input_image, verbose=0))
                        pred_map = np.squeeze(model.predict(input_image, verbose=0))
                        count_prediction += np.sum(pred_map)
                        count_prediction += np.sum(pred_map)
                        for caisse in camera["caisses"]:
                        for checkout in camera["checkouts"]:
                            if np.sum(pred_map[caisse["x1"] // 2:caisse["x2"] // 2, caisse["y1"] // 2:caisse["y2"] // 2]) > 0.5:
                            # we check whether or not the density in the checkout area is high enough to determine if it is open or not
                            if np.sum(pred_map[checkout["x1"] // 2:checkout["x2"] // 2, checkout["y1"] // 2:checkout["y2"] // 2]) > 0.5:
                                open_checkouts += 1
                                open_checkouts += 1
                    else:
                    else:
                        cams_working = False
                        cams_working = False
                    cap.release()
                    cap.release()


                if cams_working and open_checkouts:
                if cams_working and open_checkouts:
                    # the estimated waiting time is calculated and put in the database
                    waiting_time = timedelta(
                    waiting_time = timedelta(
                        seconds=restaurant['b_factor'] +
                        seconds=restaurant['b_factor'] +
                        int(count_prediction *
                        int(count_prediction *
@@ -75,4 +82,5 @@ async def handle_cameras():
                    db.add(db_record)
                    db.add(db_record)
                    db.commit()
                    db.commit()
                    await manager.broadcast(json.dumps({"type": "data"}))
                    await manager.broadcast(json.dumps({"type": "data"}))
        # sleeping enough time so that there is a 60 second delay between the start of two consecutive loops
        time.sleep(max(0, 60 - (datetime.now() - current_date).total_seconds()))
        time.sleep(max(0, 60 - (datetime.now() - current_date).total_seconds()))
+57 −0
Original line number Original line Diff line number Diff line
import React, { useEffect, useState } from "react";
import axios from "axios";

export default function ClosureForm() {
  const [places, setPlaces] = useState([]);

  const fetchPlaces = () => {
    axios
      .get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
      .then((res) => {
        setPlaces(res.data);
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const Submit = (ev) => {
    ev.preventDefault();
    let { place, beginning_date, end_date } = ev.target.elements;

    axios
      .post(`${process.env.REACT_APP_BASE_URL_BACK}/closure`, {
        place: place.value,
        beginning_date: beginning_date.value,
        end_date: end_date.value,
      })
      .then((res) => {
        console.log(res);
      })
      .catch((e) => {
        console.log(e);
      });
  };

  useEffect(fetchPlaces, []);

  return (
    <form onSubmit={Submit} id="news-form-container">
      <label htmlFor="place">Restaurant concerné</label>
      <select name="place">
        {places.map((place) => (
          <option key={place.name}>{place.name}</option>
        ))}
      </select>
      <label htmlFor="beginning_date" className="news-form-label">
        Date de fermeture
      </label>
      <input type="datetime-local" name="beginning_date" style={{ minHeight: "2rem" }} />
      <label htmlFor="end_date" className="news-form-label">
        Date de réouverture
      </label>
      <input type="datetime-local" name="end_date" style={{ minHeight: "2rem" }} />
      <input id="submit_event" type="submit" className="news-form-label" />
    </form>
  );
}
Original line number Original line Diff line number Diff line
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { useContext, useEffect, useRef, useState } from "react";
import axios from "axios";
import axios from "axios";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { AiOutlineInfoCircle, AiOutlineDelete } from "react-icons/ai";
import { BiSend } from "react-icons/bi";
import { BiSend } from "react-icons/bi";
import { BsChatText } from "react-icons/bs";
import { BsChatText } from "react-icons/bs";


@@ -9,7 +9,7 @@ import { getSiblings } from "../utils";


import "../styles/Comments.css";
import "../styles/Comments.css";


export default function Messages({ place, infos, lastMessage }) {
export default function Messages({ place, infos, lastMessage, admin }) {
  const [user] = useContext(User);
  const [user] = useContext(User);
  const [messages, setMessages] = useState([]);
  const [messages, setMessages] = useState([]);
  const [newComment, setNewComment] = useState("");
  const [newComment, setNewComment] = useState("");
@@ -35,11 +35,29 @@ export default function Messages({ place, infos, lastMessage }) {
        })
        })
        .catch((e) => {
        .catch((e) => {
          console.log(e);
          console.log(e);
          alert("Une erreur est survenue");
          updateValue("");
          updateValue("");
        });
        });
    }
    }
  };
  };


  const Delete = (ev) => {
    if (infos && admin) {
      ev.preventDefault();
      axios
        .delete(`${process.env.REACT_APP_BASE_URL_BACK}/news/${ev.target.id}`, {
          withCredentials: true,
        })
        .then(() => {
          setLoading(true);
        })
        .catch((e) => {
          alert("Une erreur est survenue");
          console.log(e);
        });
    }
  };

  const updateValue = (value) => {
  const updateValue = (value) => {
    setNewComment(value);
    setNewComment(value);
    if (input.current) {
    if (input.current) {
@@ -81,7 +99,7 @@ export default function Messages({ place, infos, lastMessage }) {
      .get(
      .get(
        `${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/${
        `${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/${
          infos ? "news" : "comments"
          infos ? "news" : "comments"
        }`,
        }${admin ? "?admin=true" : ""}`,
      )
      )
      .then((res) => {
      .then((res) => {
        setMessages(res.data);
        setMessages(res.data);
@@ -95,7 +113,7 @@ export default function Messages({ place, infos, lastMessage }) {
        console.log(e);
        console.log(e);
        setLoading(false);
        setLoading(false);
      });
      });
  }, []);
  }, [place, loading]);


  useEffect(() => {
  useEffect(() => {
    if (chat.current) {
    if (chat.current) {
@@ -127,7 +145,8 @@ export default function Messages({ place, infos, lastMessage }) {
  }, [lastMessage]);
  }, [lastMessage]);


  return (
  return (
    <div className="comments-side-bar">
    <div className={`comments-side-bar ${admin ? "comments-full-size" : ""}`}>
      {!admin && (
        <div className="comments-title">
        <div className="comments-title">
          {infos ? (
          {infos ? (
            <>
            <>
@@ -141,6 +160,7 @@ export default function Messages({ place, infos, lastMessage }) {
            </>
            </>
          )}
          )}
        </div>
        </div>
      )}
      <div ref={chat} className={`comments-scroll-bar ${infos && "infos-scroll-bar"}`}>
      <div ref={chat} className={`comments-scroll-bar ${infos && "infos-scroll-bar"}`}>
        {!messages.length ? (
        {!messages.length ? (
          loading ? (
          loading ? (
@@ -162,6 +182,13 @@ export default function Messages({ place, infos, lastMessage }) {
            return (
            return (
              <div key={index} className="comment">
              <div key={index} className="comment">
                <div className={`comment-title${infos ? "-infos" : ""}`}>
                <div className={`comment-title${infos ? "-infos" : ""}`}>
                  {admin && (
                    <AiOutlineDelete
                      id={message.id}
                      onClick={Delete}
                      className="comment-delete-button"
                    />
                  )}
                  {infos ? message.title : message.username}
                  {infos ? message.title : message.username}
                </div>
                </div>
                <div className="comment-content">{message.content}</div>
                <div className="comment-content">{message.content}</div>
Original line number Original line Diff line number Diff line
@@ -19,7 +19,7 @@ export default function Header({ selection, setSelection }) {
        .get(`${process.env.REACT_APP_BASE_URL_BACK}/auth`, { withCredentials: true })
        .get(`${process.env.REACT_APP_BASE_URL_BACK}/auth`, { withCredentials: true })
        .then((res) => {
        .then((res) => {
          setUser(res.data);
          setUser(res.data);
          localStorage.setItem("user", res.data);
          localStorage.setItem("user", JSON.stringify(res.data));
        })
        })
        .catch((e) => console.log(e));
        .catch((e) => console.log(e));
    }
    }
+70 −0
Original line number Original line Diff line number Diff line
import React, { useEffect, useState } from "react";
import axios from "axios";

import "../styles/NewsForm.css";

export default function NewsForm() {
  const [places, setPlaces] = useState([]);

  const fetchPlaces = () => {
    axios
      .get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
      .then((res) => {
        setPlaces(res.data);
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const Submit = (ev) => {
    ev.preventDefault();
    let { title, content, end_date, place } = ev.target.elements;

    axios
      .post(
        `${process.env.REACT_APP_BASE_URL_BACK}/news`,
        {
          title: title.value,
          content: content.value,
          end_date: end_date.value,
          place: place.value,
        },
        { withCredentials: true },
      )
      .then(() => {
        ev.target.reset();
        alert("L'actualité a bien été postée !");
      })
      .catch((e) => {
        console.log(e);
        alert("Une erreur est survenue");
      });
  };

  useEffect(fetchPlaces, []);

  return (
    <form onSubmit={Submit} id="news-form-container">
      <label htmlFor="title">Titre</label>
      <input name="title" />
      <label htmlFor="content" className="news-form-label">
        Contenu
      </label>
      <textarea name="content" style={{ resize: "none" }} />
      <label htmlFor="end_date" className="news-form-label">
        Date de fin
      </label>
      <input type="datetime-local" name="end_date" style={{ minHeight: "2rem" }} />
      <label htmlFor="place" className="news-form-label">
        Restaurant concerné
      </label>
      <select name="place">
        {places.map((place) => (
          <option key={place.name}>{place.name}</option>
        ))}
      </select>
      <input id="submit_event" type="submit" className="news-form-label" />
    </form>
  );
}
Original line number Original line Diff line number Diff line
@@ -3,3 +3,5 @@ export { default as Footer } from "./Footer";
export { default as WaitingTime } from "./WaitingTime";
export { default as WaitingTime } from "./WaitingTime";
export { default as Graph } from "./Graph";
export { default as Graph } from "./Graph";
export { default as Comments } from "./Comments";
export { default as Comments } from "./Comments";
export { default as NewsForm } from "./NewsForm";
export { default as ClosureForm } from "./ClosureForm";
Original line number Original line Diff line number Diff line
@@ -5,7 +5,7 @@ import useWebSocket from "react-use-websocket";
import axios from "axios";
import axios from "axios";


import { Footer, Header } from "./components";
import { Footer, Header } from "./components";
import { HomePage, RestaurantPage, NotFoundPage } from "./views";
import { HomePage, RestaurantPage, NotFoundPage, AdminPage } from "./views";


import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/css/bootstrap.min.css";
import "./styles/index.css";
import "./styles/index.css";
@@ -14,11 +14,12 @@ const socketUrl = `${process.env.REACT_APP_SOCKET_URL}/ws`;
export const User = createContext(null);
export const User = createContext(null);


export default function App() {
export default function App() {
  const storedSession = localStorage.getItem("user");
  const [restaurantsList, setRestaurantsList] = useState([]);
  const [restaurantsList, setRestaurantsList] = useState([]);
  const [selection, setSelection] = useState(null);
  const [selection, setSelection] = useState(null);
  const [loading, setLoading] = useState(true);
  const [loading, setLoading] = useState(true);
  const [reload, setReload] = useState(true);
  const [reload, setReload] = useState(true);
  const [user, setUser] = useState(localStorage.getItem("user"));
  const [user, setUser] = useState(storedSession ? JSON.parse(storedSession) : null);
  const { lastMessage } = useWebSocket(socketUrl, {
  const { lastMessage } = useWebSocket(socketUrl, {
    shouldReconnect: () => true,
    shouldReconnect: () => true,
  });
  });
@@ -78,6 +79,9 @@ export default function App() {
                path="/"
                path="/"
                element={<HomePage {...{ restaurantsList, setSelection, loading }} />}
                element={<HomePage {...{ restaurantsList, setSelection, loading }} />}
              />
              />
              {user?.admin && (
                <Route path="/admin" element={<AdminPage lastmessage={lastMessage} />} />
              )}
              <Route
              <Route
                path="/:restaurant"
                path="/:restaurant"
                element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />}
                element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />}
+31 −0
Original line number Original line Diff line number Diff line
#admin-container {
    width: 100%; 
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 1rem;
}

#admin-news-list {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 40rem;
    max-width: 90%;
    height: 100%;
    border: 1px solid white;
    padding: 1rem;
    margin: 1rem;
    margin-top: 2rem;
    overflow-y: auto;
}

.admin-select-option {
    font-style: 1.2rem;
    margin-bottom: 1rem;
}

#admin-select-place {
    width: fit-content;
}
 No newline at end of file
Original line number Original line Diff line number Diff line
@@ -32,6 +32,10 @@
    overflow: hidden;
    overflow: hidden;
}
}


.comments-full-size {
    width: 100%;
}

.comments-scroll-bar {
.comments-scroll-bar {
    flex: 1;
    flex: 1;
    display: flex;
    display: flex;
@@ -119,6 +123,10 @@
    font-size : 1.2rem;
    font-size : 1.2rem;
}
}


.comment-delete-button { 
    margin-left: 1rem;
}

@media only screen and (max-width: 600px) {
@media only screen and (max-width: 600px) {
    .comments-side-bar {
    .comments-side-bar {
        width: 0px;
        width: 0px;
+23 −0
Original line number Original line Diff line number Diff line
#news-form-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    width: 40rem;
    max-width: 90%;
    height: 100%;
    border: 1px solid white;
    padding: 1rem;
    margin: 1rem;
    margin-top: 2rem;
    overflow-y: auto;
}

.news-form-label {
    margin-top: 1rem;
}

input::placeholder {
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
}
 No newline at end of file
+66 −0
Original line number Original line Diff line number Diff line
import React, { useEffect, useState } from "react";
import axios from "axios";

import { NewsForm, ClosureForm, Comments } from "../components";

import "../styles/Admin.css";

export default function AdminPage({ lastMessage }) {
  const [form, setForm] = useState("");

  function NewsList() {
    const [place, setPlace] = useState(null);
    const [places, setPlaces] = useState([]);

    const fetchPlaces = () => {
      axios
        .get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
        .then((res) => {
          setPlaces(res.data);
          res.data.length && setPlace(res.data[0].name);
        })
        .catch((e) => {
          console.log(e);
        });
    };

    useEffect(fetchPlaces, []);

    return (
      <div id="admin-news-list">
        <select id="admin-select-place" name="place" onChange={(ev) => setPlace(ev.target.value)}>
          {places.map((place) => (
            <option key={place.name}>{place.name}</option>
          ))}
        </select>
        <Comments place={place} lastMessage={lastMessage} infos admin />
      </div>
    );
  }

  let forms = {
    news: <NewsForm />,
    newsList: <NewsList />,
    closure: <ClosureForm />,
  };

  return (
    <div id="admin-container">
      <select defaultValue="" onChange={(ev) => setForm(ev.target.value)}>
        <option className="admin-select-option" value="">
          -- Sélectionner une action --
        </option>
        <option className="admin-select-option" value="news">
          Ajouter une actualité
        </option>
        <option className="admin-select-option" value="newsList">
          Supprimer une actualité
        </option>
        <option className="admin-select-option" value="closure">
          Renseigner une fermeture exceptionnelle
        </option>
      </select>
      {form && forms[form]}
    </div>
  );
}
Original line number Original line Diff line number Diff line
export { default as HomePage } from "./HomePage";
export { default as HomePage } from "./HomePage";
export { default as RestaurantPage } from "./Restaurant";
export { default as RestaurantPage } from "./Restaurant";
export { default as NotFoundPage } from "./NotFoundPage";
export { default as NotFoundPage } from "./NotFoundPage";
export { default as AdminPage } from "./AdminPage";