Skip to content
Snippets Groups Projects
Commit f58d1284 authored by Aymeric Chaumont's avatar Aymeric Chaumont
Browse files

Merge branch 'collab_front' into 'main'

improve front

See merge request !28
parents 92796d5e 0be999eb
Branches
No related tags found
1 merge request!28improve front
Pipeline #43984 passed
Showing
with 38009 additions and 37362 deletions
...@@ -13,11 +13,38 @@ from db import models, schemas ...@@ -13,11 +13,38 @@ from db import models, schemas
def get_waiting_time(place: str, db: Session): def get_waiting_time(place: str, db: Session):
""" Get the last estimated waiting time for the given place """ """ Get the last estimated waiting time for the given place """
db_record = db.query(models.Records).filter(models.Records.place == place).order_by(models.Records.date.desc()).first() current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
return db_record.waiting_time weekday, current_time = current_date.weekday(), current_date.time()
first_timeslot = get_timeslot(place, weekday, True, db)
if first_timeslot and current_time < first_timeslot[0]:
return first_timeslot[0].hour, first_timeslot[0].minute
elif first_timeslot and current_time <= first_timeslot[1]:
waiting_time = db.query(
models.Records.waiting_time
).filter(
models.Records.place == place
).order_by(
models.Records.date.desc()
).first()
waiting_time_minutes = round(waiting_time[0].total_seconds() / 60)
return waiting_time_minutes, None
second_timeslot = get_timeslot(place, weekday, False, db)
if second_timeslot and current_time < second_timeslot[0]:
return second_timeslot[0].hour, second_timeslot[0].minute
elif second_timeslot and current_time <= second_timeslot[1]:
waiting_time = db.query(
models.Records.waiting_time
).filter(
models.Records.place == place
).order_by(
models.Records.date.desc()
).first()
waiting_time_minutes = round(waiting_time[0].total_seconds() / 60)
return waiting_time_minutes, None
return None, None
def get_stats(place: str, weekday: int, min_time_hour: int, min_time_mn: int, max_time_hour: int, max_time_mn: int, interval: timedelta, db: Session): def get_avg_graph_points(place: str, weekday: int, min_time: time, max_time: time, interval: timedelta, db: Session):
""" Get the average waiting time for each interval between two time steps """ """ Get the average waiting time for each interval between two time steps """
def shift_time(t: time, delta: timedelta): def shift_time(t: time, delta: timedelta):
...@@ -51,7 +78,6 @@ def get_stats(place: str, weekday: int, min_time_hour: int, min_time_mn: int, ma ...@@ -51,7 +78,6 @@ def get_stats(place: str, weekday: int, min_time_hour: int, min_time_mn: int, ma
name = f'{start_time.hour:02}h{start_time.minute:02}' name = f'{start_time.hour:02}h{start_time.minute:02}'
slots_list.append({'name': name, 'time': average_waiting_time}) slots_list.append({'name': name, 'time': average_waiting_time})
min_time, max_time = time(min_time_hour, min_time_mn), time(max_time_hour, max_time_mn)
stats = [] stats = []
start_time, end_time = min_time, shift_time(min_time, interval) start_time, end_time = min_time, shift_time(min_time, interval)
while start_time < max_time: while start_time < max_time:
...@@ -61,17 +87,93 @@ def get_stats(place: str, weekday: int, min_time_hour: int, min_time_mn: int, ma ...@@ -61,17 +87,93 @@ def get_stats(place: str, weekday: int, min_time_hour: int, min_time_mn: int, ma
return stats return stats
def get_avg_graph(place: str, db: Session):
""" Get the average waiting time for each interval between two time steps,
for the current or next available timeslot"""
current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
weekday, current_time = current_date.weekday(), current_date.time()
first_timeslot = get_timeslot(place, weekday, True, db)
if first_timeslot and current_time <= first_timeslot[1]:
return get_avg_graph_points(place, weekday, first_timeslot[0], first_timeslot[1], timedelta(minutes=5), db)
second_timeslot = get_timeslot(place, weekday, False, db)
if second_timeslot and current_time <= second_timeslot[1]:
return get_avg_graph_points(place, weekday, second_timeslot[0], second_timeslot[1], timedelta(minutes=5), db)
return None
def get_current_graph_points(place: str, current_date: date, min_time: time, max_time: time, interval: timedelta, db: Session):
""" Get the waiting time for each interval between two time steps for the current timeslot """
def shift_time(t: time, delta: timedelta):
return (datetime.combine(date(1, 1, 1), t) + delta).time()
def avg_time_query(start_time, end_time):
records = db.query(
(func.round(
func.avg(
3600 * func.extract('HOUR', models.Records.waiting_time) +
60 * func.extract('MINUTE', models.Records.waiting_time) +
func.extract('SECOND', models.Records.waiting_time))
)) / 60
).filter(
models.Records.place == place,
func.extract('YEAR', models.Records.date) == current_date.year,
func.extract('MONTH', models.Records.date) == current_date.month,
func.extract('DAY', models.Records.date) == current_date.day,
(func.extract('HOUR', models.Records.date) > start_time.hour) |
((func.extract('HOUR', models.Records.date) == start_time.hour) &
(func.extract('MINUTE', models.Records.date) >= start_time.minute)),
(func.extract('HOUR', models.Records.date) < end_time.hour) |
((func.extract('HOUR', models.Records.date) == end_time.hour) &
(func.extract('MINUTE', models.Records.date) < end_time.minute)),
).one()
if records[0]:
return int(records[0])
return None
def add_slot(slots_list, start_time, end_time):
average_waiting_time = avg_time_query(start_time, end_time)
if average_waiting_time:
name = f'{start_time.hour:02}h{start_time.minute:02}'
slots_list.append({'name': name, 'time': average_waiting_time})
stats = []
start_time, end_time = min_time, shift_time(min_time, interval)
while start_time < max_time:
add_slot(stats, start_time, end_time)
start_time, end_time = end_time, shift_time(end_time, interval)
return stats
def get_current_graph(place: str, db: Session):
""" Get the waiting_time_graph for the current timeslot"""
current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
weekday, day, current_time = current_date.weekday(), current_date.date(), current_date.time()
first_timeslot = get_timeslot(place, weekday, True, db)
if first_timeslot and current_time <= first_timeslot[0]:
return None
elif first_timeslot and current_time <= first_timeslot[1]:
return get_current_graph_points(place, day, first_timeslot[0], current_time, timedelta(minutes=5), db)
second_timeslot = get_timeslot(place, weekday, False, db)
if second_timeslot and current_time <= second_timeslot[0]:
return None
elif second_timeslot and current_time <= second_timeslot[1]:
return get_current_graph_points(place, day, second_timeslot[0], current_time, timedelta(minutes=5), db)
return None
# Define CRUD operation for the comments # Define CRUD operation for the comments
def get_comments(place: str, page: int, db: Session): def get_comments(place: str, page: int, db: Session):
""" Get the 10 last comments for the given place """ """ Get the 10 last comments for the given place """
if page == 0: if page == 0:
comments = db.query(models.Comments).order_by(models.Comments.date.desc(), models.Comments.id.desc()).all() comments = db.query(models.Comments).order_by(models.Comments.published_at.desc(), models.Comments.id.desc()).all()
else: else:
comments = db.query( comments = db.query(
models.Comments).filter( models.Comments).filter(
models.Comments.place == place).order_by( models.Comments.place == place).order_by(
models.Comments.date.desc(), models.Comments.published_at.desc(),
models.Comments.id.desc()).slice( models.Comments.id.desc()).slice(
(page - (page -
1) * 1) *
...@@ -84,7 +186,7 @@ def get_comments(place: str, page: int, db: Session): ...@@ -84,7 +186,7 @@ def get_comments(place: str, page: int, db: Session):
def create_comment(place: str, new_comments: schemas.CommentBase, db: Session): def create_comment(place: str, new_comments: schemas.CommentBase, db: Session):
""" Add a new comment to the database """ """ Add a new comment to the database """
date = datetime.now(tz=pytz.timezone("Europe/Paris")) date = datetime.now(tz=pytz.timezone("Europe/Paris"))
db_comment = models.Comments(**new_comments.dict(), date=date, place=place) db_comment = models.Comments(**new_comments.dict(), published_at=date, place=place)
db.add(db_comment) db.add(db_comment)
db.commit() db.commit()
db.refresh(db_comment) db.refresh(db_comment)
...@@ -104,7 +206,7 @@ def delete_comment(id: int, db: Session): ...@@ -104,7 +206,7 @@ def delete_comment(id: int, db: Session):
def get_news(place: str, db: Session): def get_news(place: str, db: Session):
""" Get the news for the given place """ """ Get the news for the given place """
news = db.query(models.News).filter(models.News.place == place).order_by(models.News.date.desc()).all() news = db.query(models.News).filter(models.News.place == place).order_by(models.News.published_at.desc()).all()
return news return news
...@@ -125,3 +227,102 @@ def delete_news(id: int, db: Session): ...@@ -125,3 +227,102 @@ def delete_news(id: int, db: Session):
else: else:
db.query(models.News).filter(models.News.id == id).delete() db.query(models.News).filter(models.News.id == id).delete()
db.commit() db.commit()
# Define CRUD operation for the opening hours
def get_opening_hours(place: str, db: Session):
""" Get the opening hours for the given place """
opening_hours = db.query(
models.OpeningHours.day,
models.OpeningHours.timeslot,
models.OpeningHours.open_time,
models.OpeningHours.close_time,
).filter(
models.OpeningHours.place == place
).order_by(
models.OpeningHours.day, models.OpeningHours.timeslot.desc()
).all()
return opening_hours
def get_timeslot(place: str, day: int, timeslot: bool, db: Session):
""" Get the opening hours for the given place and timeslot"""
opening_hours = db.query(
models.OpeningHours.open_time,
models.OpeningHours.close_time,
).filter(
models.OpeningHours.place == place,
models.OpeningHours.day == day,
models.OpeningHours.timeslot == timeslot
).first()
return opening_hours
def create_opening_hours(new_opening_hours: schemas.OpeningHoursBase, db: Session):
""" Add opening hours to the database """
db_opening_hours = models.OpeningHours(**new_opening_hours.dict())
db.add(db_opening_hours)
db.commit()
db.refresh(db_opening_hours)
return db_opening_hours
def delete_opening_hours(id: int, db: Session):
""" Delete the opening hours with the matching id """
if id == 0:
db.query(models.OpeningHours).delete()
else:
db.query(models.OpeningHours).filter(models.OpeningHours.id == id).delete()
db.commit()
# Restaurants information
def get_restaurants(db: Session):
current_date = datetime.now(tz=pytz.timezone("Europe/Paris"))
weekday, current_time = current_date.weekday(), current_date.time()
restaurant_names = [r.place for r in db.query(models.OpeningHours.place.distinct).distinct()]
restaurants = []
for name in restaurant_names:
restaurant = {}
restaurant["name"] = name
first_timeslot = get_timeslot(name, weekday, True, db)
second_timeslot = get_timeslot(name, weekday, False, db)
if (first_timeslot and first_timeslot[0] <= current_time < first_timeslot[1]) or (
second_timeslot and second_timeslot[0] <= current_time < second_timeslot[1]):
restaurant["status"] = True
else:
restaurant["status"] = False
if first_timeslot and second_timeslot:
restaurant["timetable"] = (
f"{first_timeslot[0].hour:{'0'}{2}}h{first_timeslot[0].minute:{'0'}{2}}-"
f"{first_timeslot[1].hour:{'0'}{2}}h{first_timeslot[1].minute:{'0'}{2}} / "
f"{second_timeslot[0].hour:{'0'}{2}}h{second_timeslot[0].minute:{'0'}{2}}-"
f"{second_timeslot[1].hour:{'0'}{2}}h{second_timeslot[1].minute:{'0'}{2}}")
elif first_timeslot:
restaurant["timetable"] = (
f"{first_timeslot[0].hour:{'0'}{2}}h{first_timeslot[0].minute:{'0'}{2}}-"
f"{first_timeslot[1].hour:{'0'}{2}}h{first_timeslot[1].minute:{'0'}{2}}")
else:
restaurant["timeslot"] = "-"
if restaurant["status"]:
waiting_time = db.query(
models.Records.waiting_time
).filter(
models.Records.place == name
).order_by(
models.Records.date.desc()
).first()
waiting_time_minutes = round(waiting_time[0].total_seconds() / 60)
restaurant["waiting_time"] = waiting_time_minutes
else:
restaurant["waiting_time"] = None
restaurants.append(restaurant)
return restaurants
""" """
Models of the database for magasin app Models of the database for magasin app
""" """
from sqlalchemy import Column, Integer, DateTime, Float, Interval, String, Text from sqlalchemy import Column, Integer, DateTime, Float, Interval, String, Text, Boolean, Time
from db.database import Base from db.database import Base
...@@ -11,7 +11,7 @@ class Records(Base): ...@@ -11,7 +11,7 @@ class Records(Base):
__tablename__ = "records" __tablename__ = "records"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
place = Column(String(10)) place = Column(String(30))
date = Column(DateTime) date = Column(DateTime)
density = Column(Float) density = Column(Float)
waiting_time = Column(Interval) waiting_time = Column(Interval)
...@@ -22,18 +22,30 @@ class Comments(Base): ...@@ -22,18 +22,30 @@ class Comments(Base):
__tablename__ = "comments" __tablename__ = "comments"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
comment = Column(Text) content = Column(Text)
date = Column(DateTime) published_at = Column(DateTime)
place = Column(String(10)) place = Column(String(30))
class News(Base): class News(Base):
"""Records sql table model""" """News sql table model"""
__tablename__ = "News sql table model" __tablename__ = "news"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
title = Column(String(50)) title = Column(String(50))
content = Column(Text) content = Column(Text)
published_at = Column(DateTime) published_at = Column(DateTime)
end_date = Column(DateTime) end_date = Column(DateTime)
place = Column(String(10)) place = Column(String(30))
class OpeningHours(Base):
"""OpeningHours sql table model"""
__tablename__ = "opening_hours"
id = Column(Integer, primary_key=True, index=True)
place = Column(String(30))
day = Column(Integer)
timeslot = Column(Boolean)
open_time = Column(Time)
close_time = Column(Time)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Pydantic schemas for the magasin app Pydantic schemas for the magasin app
""" """
from typing import Optional from typing import Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta, time
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
...@@ -24,13 +24,13 @@ class Record(RecordBase): ...@@ -24,13 +24,13 @@ class Record(RecordBase):
class CommentBase(BaseModel): class CommentBase(BaseModel):
"""Comments base schema""" """Comments base schema"""
comment: str = Field(..., title="Content of the comment posted") content: str = Field(..., title="Content of the comment posted")
class Comment(CommentBase): class Comment(CommentBase):
"""Database comments base schema""" """Database comments base schema"""
id: int id: int
date: datetime = Field(..., title="Publication date of the comment") published_at: datetime = Field(..., title="Publication date of the comment")
place: str = Field(..., title="Name of the RU corresponding the comment") place: str = Field(..., title="Name of the RU corresponding the comment")
class Config: class Config:
...@@ -52,3 +52,20 @@ class News(NewsBase): ...@@ -52,3 +52,20 @@ class News(NewsBase):
class Config: class Config:
orm_mode = True orm_mode = True
class OpeningHoursBase(BaseModel):
"""Database opening_hours base schema"""
place: str = Field(..., title="Name of the RU corresponding the given record")
day: int = Field(..., title="Day of the week")
timeslot: bool = Field(..., title="Service slot (True for midday, False for evening)")
open_time: time = Field(..., title="Opening time")
close_time: time = Field(..., title="Closing time")
class OpeningHours(OpeningHoursBase):
"""Database opening_hours base schema"""
id: int
class Config:
orm_mode = True
...@@ -2,6 +2,11 @@ from fastapi import FastAPI ...@@ -2,6 +2,11 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
from db.database import get_db
from fastapi import Depends
from sqlalchemy.orm import Session
from db import schemas
from typing import List
from db import database, models from db import database, models
from routers import stats, comments, news from routers import stats, comments, news
...@@ -36,6 +41,30 @@ app.include_router(comments.router) ...@@ -36,6 +41,30 @@ app.include_router(comments.router)
app.include_router(news.router) app.include_router(news.router)
@app.get('/api/records', response_model=List[schemas.Record])
async def get_records(place: str, db: Session = Depends(get_db)):
return db.query(models.Records).filter(models.Records == place).order_by(models.Records.date.desc()).all()
@app.post('/api/records', response_model=schemas.Record)
async def stats(record: schemas.RecordBase, db: Session = Depends(get_db)):
db_record = models.Records(**record.dict())
db.add(db_record)
db.commit()
db.refresh(db_record)
return db_record
@app.delete('/api/records', response_model=None)
async def stats(id: str, db: Session = Depends(get_db)):
if id == 0:
db.query(models.Records).delete()
else:
db.query(models.Records).filter(models.Records.id == id).delete()
db.commit()
return
""" """
import cv2 import cv2
import numpy as np import numpy as np
......
...@@ -14,9 +14,9 @@ async def get_news(place: str, db: Session = Depends(get_db)): ...@@ -14,9 +14,9 @@ async def get_news(place: str, db: Session = Depends(get_db)):
return crud.get_news(place, db) return crud.get_news(place, db)
@router.post('/{place}/news', response_model=schemas.News) @router.post('/news', response_model=schemas.News)
async def create_news(place: str, news: schemas.NewsBase, db: Session = Depends(get_db)): async def create_news(news: schemas.NewsBase, db: Session = Depends(get_db)):
return crud.create_news(place, news, db) return crud.create_news(news, db)
@router.delete('/news/{id}', response_model=None) @router.delete('/news/{id}', response_model=None)
......
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from typing import List
from db import schemas, crud
from db.database import get_db
router = APIRouter(prefix="/api", tags=["opening_hours"])
@router.get('/{place}/opening_hours', response_model=List[schemas.OpeningHours])
async def get_opening_hours(place: str, db: Session = Depends(get_db)):
return crud.get_opening_hours(place, db)
@router.get('/{place}/opening_hours/{day}/{timeslot}', response_model=List[schemas.OpeningHours])
async def get_timeslot(place: str, day: int, timeslot: bool, db: Session = Depends(get_db)):
return crud.get_timeslot(place, day, timeslot, db)
@router.post('/opening_hours', response_model=schemas.OpeningHours)
async def create_opening_hours(opening_hours: schemas.OpeningHoursBase, db: Session = Depends(get_db)):
return crud.create_opening_hours(opening_hours, db)
@router.delete('/opening_hours/{id}', response_model=None)
async def delete_opening_hours(id: int, db: Session = Depends(get_db)):
return crud.delete_opening_hours(id, db)
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from datetime import timedelta
from typing import List from typing import List
from db import crud from db import schemas, crud
from db.database import get_db from db.database import get_db
router = APIRouter(prefix="/api", tags=["stats"]) router = APIRouter(prefix="/api", tags=["stats"])
@router.get('/{place}/waiting_time', response_model=timedelta) @router.get('/{place}/waiting_time', response_model=tuple)
async def waiting_time(place: str, db: Session = Depends(get_db)): async def waiting_time(place: str, db: Session = Depends(get_db)):
return crud.get_waiting_time(place, db) return crud.get_waiting_time(place, db)
@router.get('/{place}/stats/{day}/{min_time_hour}/{min_time_mn}/{max_time_hour}/{max_time_mn}/{interval}', response_model=list) @router.get('/{place}/stats/avg_graph', response_model=list)
async def stats(place: str, day: int, min_time_hour: int, min_time_mn: int, async def stats(place: str, db: Session = Depends(get_db)):
max_time_hour: int, max_time_mn: int, interval: timedelta, db: Session = Depends(get_db)): return crud.get_avg_graph(place, db)
return crud.get_stats(place, day, min_time_hour, min_time_mn, max_time_hour, max_time_mn, interval, db)
@router.get('/{place}/stats/current_graph', response_model=list)
async def stats(place: str, db: Session = Depends(get_db)):
return crud.get_current_graph(place, db)
@router.get('/{place}/opening_hours', response_model=List[schemas.OpeningHours])
async def get_opening_hours(place: str, db: Session = Depends(get_db)):
return crud.get_opening_hours(place, db)
@router.get('/{place}/opening_hours/{day}/{timeslot}', response_model=List[schemas.OpeningHours])
async def get_timeslot(place: str, day: int, timeslot: bool, db: Session = Depends(get_db)):
return crud.get_timeslot(place, day, timeslot, db)
@router.post('/opening_hours', response_model=schemas.OpeningHours)
async def create_opening_hours(opening_hours: schemas.OpeningHoursBase, db: Session = Depends(get_db)):
return crud.create_opening_hours(opening_hours, db)
@router.delete('/opening_hours/{id}', response_model=None)
async def delete_opening_hours(id: int, db: Session = Depends(get_db)):
return crud.delete_opening_hours(id, db)
@router.get('/restaurants', response_model=List[dict])
async def get_restaurants(db: Session = Depends(get_db)):
return crud.get_restaurants(db)
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"react-bootstrap": "^2.4.0", "react-bootstrap": "^2.4.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-icons": "^4.4.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spring": "^9.4.5", "react-spring": "^9.4.5",
...@@ -16661,6 +16662,14 @@ ...@@ -16661,6 +16662,14 @@
"react": ">=16.3.0" "react": ">=16.3.0"
} }
}, },
"node_modules/react-icons": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz",
"integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
...@@ -33425,6 +33434,12 @@ ...@@ -33425,6 +33434,12 @@
"react-side-effect": "^2.1.0" "react-side-effect": "^2.1.0"
} }
}, },
"react-icons": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz",
"integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==",
"requires": {}
},
"react-is": { "react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"react-bootstrap": "^2.4.0", "react-bootstrap": "^2.4.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-icons": "^4.4.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spring": "^9.4.5", "react-spring": "^9.4.5",
......
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import axios from "axios"; import axios from "axios";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { BiSend } from "react-icons/bi";
import { BsChatText } from "react-icons/bs";
import { getSiblings } from "../utils";
import "../styles/Comments.css"; import "../styles/Comments.css";
export default function Comments({ place }) { export default function Messages({ place, infos }) {
const [comments, setComments] = useState([]); const [messages, setMessages] = useState([]);
const [newComment, setNewComment] = useState(""); const [newComment, setNewComment] = useState("");
const [loading, setLoading] = useState(true);
const input = useRef(); const input = useRef();
const chat = useRef(); const chat = useRef();
let width = window.innerWidth > 0 ? window.innerWidth : screen.width;
width = width > 600;
const Submit = (ev) => { const Submit = (ev) => {
if (newComment.replace(/\s/g, "").length) { if (newComment.replace(/\s/g, "").length) {
ev.preventDefault(); ev.preventDefault();
axios axios
.post(`${process.env.REACT_APP_BASE_URL_BACK}/${place}/comments`, { .post(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/comments`, {
comment: newComment, content: newComment,
}) })
.then((res) => { .then((res) => {
let update = comments.map((_, index) => (index ? comments[index - 1] : res.data)); if (messages.length) {
update.push(comments[comments.length - 1]); let update = messages.map((_, index) => (index ? messages[index - 1] : res.data));
setComments(update); update.push(messages[messages.length - 1]);
setMessages(update);
} else {
setMessages([res.data]);
}
updateValue(""); updateValue("");
}) })
.catch((e) => { .catch((e) => {
...@@ -37,13 +50,49 @@ export default function Comments({ place }) { ...@@ -37,13 +50,49 @@ export default function Comments({ place }) {
} }
}; };
const OpenSideBar = (ev) => {
if (!width && ev.target.parentNode.parentNode.className == "comments-side-bar") {
ev.preventDefault();
ev.stopPropagation();
let sidebar = ev.target.parentNode.parentNode;
if (!parseInt(sidebar.style.width, 10)) {
let siblings = getSiblings(sidebar);
for (let sibling of siblings) {
sibling.style.width = 0;
}
if (ev.target.id == "comments-icon-left") {
let otherIcon = document.getElementById("comments-icon-right");
otherIcon.style.cssText = "background-color: none";
} else if (ev.target.id == "comments-icon-right") {
let otherIcon = document.getElementById("comments-icon-left");
otherIcon.style.cssText = "background-color: none";
}
sidebar.style.width = "100%";
ev.target.style.cssText = "background-color: white; color: black";
} else {
let mainPage = document.getElementById("restaurant-main-page");
mainPage.style.width = "100%";
sidebar.style.width = 0;
ev.target.style.cssText = "background-color: none; color: white";
}
}
};
useEffect(() => { useEffect(() => {
axios axios
.get(`${process.env.REACT_APP_BASE_URL_BACK}/${place}/comments`) .get(
`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/${
infos ? "news" : "comments"
}`,
)
.then((res) => { .then((res) => {
setComments(res.data); setMessages(res.data);
setLoading(false);
}) })
.catch((e) => console.log(e)); .catch((e) => {
console.log(e);
setLoading(false);
});
}, []); }, []);
useEffect(() => { useEffect(() => {
...@@ -53,32 +102,71 @@ export default function Comments({ place }) { ...@@ -53,32 +102,71 @@ export default function Comments({ place }) {
} }
}, [chat.current]); }, [chat.current]);
useEffect(() => {
if (input.current) {
input.current.style.height = "";
input.current.style.height = `${input.current.scrollHeight + 5}px`;
}
}, [newComment]);
return (
<div className="comments-side-bar">
<div className="comments-title">
{infos ? (
<>
<AiOutlineInfoCircle id="comments-icon-left" onClick={OpenSideBar} />
Infos
</>
) : (
<>
<BsChatText id="comments-icon-right" onClick={OpenSideBar} />
Commentaires
</>
)}
</div>
<div ref={chat} className={`comments-scroll-bar ${infos && "infos-scroll-bar"}`}>
{!messages.length ? (
loading ? (
<div className={`comments-loading ${infos && "infos-loading"}`}>Chargement...</div>
) : (
<div className="no-comments">
<div>
Il n&apos;y a{" "}
{infos
? `aucune information particulière concernant ${place}`
: `aucun commentaire sur ${place}`}
</div>
</div>
)
) : (
messages.map((message, index) => {
let [date, hour] = message.published_at.split("T");
let [year, month, day] = date.split("-");
return ( return (
<div id="comments-side-bar">
<div ref={chat} id="comments-scroll-bar">
{comments.map((comment, index) => (
<div key={index} className="comment"> <div key={index} className="comment">
<div className="comment-content">{comment.comment}</div> <div className="comment-content">{message.content}</div>
<div className="comment-date"> <div className="comment-date">
{comment.date {${hour.substring(0, 5)} le ${day}/${month}/${year}`}
.split("T")
.reduce((date, hours) => ${hours.substring(0, 5)} le ${date}`)}
</div> </div>
</div> </div>
))} );
})
)}
</div> </div>
<div id="comment-input-container"> {!infos && (
<div className="comment-input-container">
<textarea <textarea
id="comments-input" className="comments-input"
ref={input} ref={input}
value={newComment} value={newComment}
onChange={(ev) => updateValue(ev.target.value)} onChange={(ev) => updateValue(ev.target.value)}
placeholder="Ajouter un commentaire..." placeholder="Ajouter un commentaire"
/> />
<button id="comment-input-button" onClick={Submit}> <button className="comment-input-button" onClick={Submit}>
Envoyer <BiSend />
</button> </button>
</div> </div>
)}
</div> </div>
); );
} }
...@@ -7,7 +7,7 @@ export default function Footer() { ...@@ -7,7 +7,7 @@ export default function Footer() {
<footer className="footer"> <footer className="footer">
<div className="py-2 bg-dark"> <div className="py-2 bg-dark">
<div className="container text-center"> <div className="container text-center">
Fait par{" "} Développé par{" "}
<a href="https://viarezo.fr" className="VR-link"> <a href="https://viarezo.fr" className="VR-link">
ViaRézo ViaRézo
</a> </a>
......
...@@ -11,38 +11,19 @@ import { ...@@ -11,38 +11,19 @@ import {
} from "recharts"; } from "recharts";
import "../styles/Graph.css"; import "../styles/Graph.css";
export default function DailyGraph({ export default function Graph({ place, type }) {
place, const [data, setData] = React.useState(null);
day,
min_time_hour,
min_time_mn,
max_time_hour,
max_time_mn,
interval,
}) {
const url =
process.env.REACT_APP_BASE_URL_BACK +
"/" +
place +
"/stats/" +
day +
"/" +
min_time_hour +
"/" +
min_time_mn +
"/" +
max_time_hour +
"/" +
max_time_mn +
"/" +
interval;
const [data, setData] = React.useState([]);
React.useEffect(() => { React.useEffect(() => {
axios.get(url).then((response) => { axios
.get(
`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(
place,
)}/stats/${encodeURIComponent(type)}_graph`,
)
.then((response) => {
setData(response.data); setData(response.data);
}); });
}, [url]); }, []);
if (!data) return null; if (!data) return null;
const CustomTooltip = ({ active, payload }) => { const CustomTooltip = ({ active, payload }) => {
...@@ -57,10 +38,7 @@ export default function DailyGraph({ ...@@ -57,10 +38,7 @@ export default function DailyGraph({
}; };
return ( return (
<> <div style={{ height: "60%", padding: "3rem" }}>
<div className="graph-title">
Temps d&apos;attente pour le prochain créneau d&apos;ouverture
</div>
<div className="graph"> <div className="graph">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart <AreaChart
...@@ -110,6 +88,7 @@ export default function DailyGraph({ ...@@ -110,6 +88,7 @@ export default function DailyGraph({
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</> <div className="graph-title">Temps d&apos;attente estimé depuis l&apos;ouverture</div>
</div>
); );
} }
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Nav, Navbar, NavLink } from "react-bootstrap";
export default function Header() { import "../styles/Header.css";
export default function Header({ selection, setSelection }) {
let width = window.innerWidth > 0 ? window.innerWidth : screen.width;
width = width > 600;
return ( return (
<Navbar collapseOnSelect expand="sm" bg="dark" variant="dark"> <div id="header-container">
<Navbar.Toggle <div id="header-restaurant-status">
aria-controls="navbarScroll" {!selection
data-bs-toggle="collapse" ? "Accueil"
data-bs-target="#navbarScroll" : `${selection.name} : actuellement ${selection.status ? "ouvert" : "fermé"}`}
/> </div>
<Navbar.Collapse id="navbarScroll"> <Link id="header-home-link" to="/" onClick={() => setSelection(null)}>
<Nav> <h2>{width || !selection ? "Eatfast" : selection.name}</h2>
<NavLink eventKey="1" as={Link} to="/"> </Link>
Accueil <div id="header-timetable">{selection && `horaires : ${selection.timetable}`}</div>
</NavLink> </div>
<NavLink eventKey="2" as={Link} to="/eiffel">
RU Eiffel
</NavLink>
</Nav>
</Navbar.Collapse>
</Navbar>
); );
} }
import React from "react"; import React, { useState } from "react";
import axios from "axios"; import axios from "axios";
import "../styles/WaitingTime.css"; import "../styles/WaitingTime.css";
export default function WaitingTime({ place }) { export default function WaitingTime({ place }) {
const url = process.env.REACT_APP_BASE_URL_BACK + "/" + place + "/waiting_time"; const [post, setPost] = useState([null, null]);
const [post, setPost] = React.useState(null);
React.useEffect(() => { React.useEffect(() => {
axios.get(url).then((response) => { axios
if (response.data < 60) { .get(`${process.env.REACT_APP_BASE_URL_BACK}/${encodeURIComponent(place)}/waiting_time`)
setPost(0); .then((res) => {
} else { setPost(res.data);
setPost(Math.round(response.data / 60));
}
}); });
}, [url]); }, []);
return ( return (
<div className="parent"> <div id="waiting-time-parent">
{post ? ( {post[1] ? (
<div className="waiting-time"> <div id="waiting-time-display">
Temps d&apos;attente estimé à <b>{post} minutes</b>. Le RU ouvre aujourd&apos;hui à :
<div className="waiting-time-minutes">
<b id="waiting-time-number">
{String(post[0]).padStart(2, "0")}h{String(post[1]).padStart(2, "0")}
</b>
</div>
</div>
) : post[0] ? (
<div id="waiting-time-display">
Le temps d&apos;attente est estimé à :
<div className="waiting-time-minutes">
<b id="waiting-time-number">{post[0]}</b> minutes
</div>
</div> </div>
) : ( ) : (
<div>Pas de données...</div> <div className="waiting-time-minutes">Le RU est fermé pour aujourd&apos;hui.</div>
)} )}
</div> </div>
); );
......
...@@ -2,5 +2,5 @@ export { default as Header } from "./Header"; ...@@ -2,5 +2,5 @@ export { default as Header } from "./Header";
export { default as Footer } from "./Footer"; export { default as Footer } from "./Footer";
export { default as Timetable } from "./Timetable"; export { default as Timetable } from "./Timetable";
export { default as WaitingTime } from "./WaitingTime"; export { default as WaitingTime } from "./WaitingTime";
export { default as DailyGraph } from "./Graph"; export { default as Graph } from "./Graph";
export { default as Comments } from "./Comments"; export { default as Comments } from "./Comments";
import React from "react"; import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import axios from "axios";
import { Header, Footer } from "./components"; import { Footer, Header } from "./components";
import { HomePage, Eiffel, NotFoundPage } from "./views"; import { HomePage, RestaurantPage, NotFoundPage, DetailsPage } from "./views";
import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap/dist/css/bootstrap.min.css";
import "./styles/index.css"; import "./styles/index.css";
export default function App() { export default function App() {
const [restaurantsList, setRestaurantsList] = useState([]);
const [selection, setSelection] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios
.get(`${process.env.REACT_APP_BASE_URL_BACK}/restaurants`)
.then((res) => {
setRestaurantsList(res.data);
setLoading(false);
})
.catch((e) => {
console.log(e);
setLoading(false);
});
}, []);
useEffect(() => {
let path = window.location.pathname.split("/");
if (restaurantsList && path.length >= 2 && path[1]) {
let name = decodeURIComponent(path[1]);
let filter = restaurantsList.filter((restaurant) => restaurant.name == name);
if (filter) {
setSelection(filter[0]);
}
}
}, [restaurantsList]);
return ( return (
<div className="app"> <div className="app">
<Router> <Router>
<Header /> <Header {...{ selection, setSelection }} />
<div className="page"> <div className="page">
<Routes> <Routes>
<Route exact path="/" element={<HomePage />} /> <Route
<Route path="/eiffel" element={<Eiffel />} /> exact
<Route path="/*" element={<NotFoundPage />} /> path="/"
element={<HomePage {...{ restaurantsList, setSelection, loading }} />}
/>
<Route
path="/:restaurant"
element={<RestaurantPage {...{ selection, setSelection }} />}
>
<Route path="details" element={<DetailsPage selection={selection} />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</div> </div>
<Footer />
</Router> </Router>
<Footer />
</div> </div>
); );
} }
ReactDOM.render( const container = document.getElementById("root");
<React.StrictMode> const root = createRoot(container);
<App /> root.render(<App />);
</React.StrictMode>,
document.getElementById("root"),
);
#comments-input { .comments-title {
font-size: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
}
.comments-input {
resize: none; resize: none;
display : block; display : block;
height: 2.4rem; height: 2.4rem;
...@@ -10,18 +18,30 @@ ...@@ -10,18 +18,30 @@
border-radius: 0.5rem; border-radius: 0.5rem;
} }
#comments-side-bar { .comments-input::placeholder {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.comments-side-bar {
width: 25%; width: 25%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
overflow: hidden;
} }
#comments-scroll-bar { .comments-scroll-bar {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
overflow-y: scroll; overflow-y: auto;
}
.infos-scroll-bar {
direction: rtl;
flex-direction: column;
} }
.comment { .comment {
...@@ -45,20 +65,84 @@ ...@@ -45,20 +65,84 @@
margin-left: 0.2rem; margin-left: 0.2rem;
} }
#comment-input-container { .comment-input-container {
display: flex; display: flex;
width: 100%; width: 100%;
background-color: rgb(59, 137, 255); padding-left: 1rem;
} }
#comment-input-button { .comment-input-button {
display: flex;
justify-content: center;
align-items: center;
background-color: rgb(17, 2, 145); background-color: rgb(17, 2, 145);
color: white; color: white;
height: 2.4rem; height: 2.4rem;
width: 2.4rem;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.2rem; padding: 0.2rem;
font-size: 1.5rem;
}
.comment-input-button:hover {
background-color: rgb(18, 0, 177);
}
.no-comments {
height: 100%;
display: flex;
align-items: center;
padding: 1.5rem;
justify-content: center;
}
.comments-loading {
margin-top: 2rem;
height: 100%;
}
.infos-loading {
direction: ltr;
}
#comments-icon-left, #comments-icon-right {
height: 2rem;
width: 2rem;
margin-right: 0.5rem;
}
@media only screen and (max-width: 600px) {
.comments-side-bar {
width: 0px;
}
#comments-icon-left, #comments-icon-right {
position: absolute;
top: 0;
height: 3rem;
width: 3rem;
margin: 1rem;
padding: 0.5rem;
border-radius: 5px;
} }
#comment-input-button:hover { #comments-icon-left {
background-color: rgb(20, 0, 196); left: 0;
}
#comments-icon-right {
right: 0;
}
.comments-title {
font-size: 2rem;
}
.no-comments {
font-size: 1.4rem;
}
.comment-input-container {
padding-left: 0rem;
}
} }
\ No newline at end of file
#header-container {
display: flex;
justify-content: space-between;
align-items: baseline;
padding-left: 1rem;
padding-right: 1rem;
background-color: rgb(33, 37, 41);
}
#header-home-link {
text-decoration: none;
color: inherit;
}
#header-restaurant-status {
flex: 1;
text-align: left;
font-size: 1.2rem;
}
#header-timetable {
flex: 1;
text-align: right;
font-weight: lighter;
}
@media only screen and (max-width: 600px) {
#header-home-link > h2 {
font-size: 2rem;
}
#header-restaurant-status {
display: none;
}
#header-container {
flex-direction: column;
align-items: center;
padding-top: .5rem;
padding-bottom: .5rem;
}
}
\ No newline at end of file
#home-container {
padding-top: 2%;
padding-bottom: 10%;
display: flex;
flex-direction: column;
align-content: center;
height: 100%;
}
#home-selection-title {
margin-bottom: 0;
padding-bottom: 2%;
}
#home-table {
width: fit-content;
align-self: center;
min-height: 100%;
}
.home-restaurant-name {
align-items: center;
padding-right: 5rem;
}
.home-arrow-reverse {
display: flex;
flex-direction: row-reverse;
justify-content: left;
}
.home-link-item {
color: inherit;
font-weight: bold;
text-decoration: none;
white-space: nowrap;
}
.home-link-item:hover {
color: inherit;
}
.home-link-item:hover + span {
color: rgb(45, 45, 45);
}
.home-arrow {
margin-right: 1rem;
}
.home-restaurant-status {
white-space: nowrap;
padding-right: 5rem;
text-align: left;
}
.home-waiting-time {
white-space: nowrap;
text-align: left;
}
@media only screen and (max-width: 600px) {
#home-selection-title {
padding-top: 10%;
margin-bottom: 0;
padding-bottom: 8%;
}
#home-table {
min-height: 70%;
}
.home-restaurant-status {
display: none;
}
.home-restaurant-name {
padding-right: 2rem;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment