diff --git a/backend/db/crud.py b/backend/db/crud.py index 6315054655b56f50af8d58009d7c874dd7f8a7d3..f77b3ac7efbe43cfc8cee867f03e6d06d7df94a3 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -1,127 +1,127 @@ -""" -Module to interact with the database -""" -from datetime import date, datetime, time, timedelta -from sqlalchemy.orm import Session -from sqlalchemy.sql import func -import pytz - -from db import models, schemas - - -# Define CRUD operation to collect the statistics - -def get_waiting_time(place: str, db: Session): - """ 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() - return db_record.waiting_time - - -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): - """ Get the average waiting time for each interval between two time steps """ - - 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.weekday(models.Records.date) == weekday, - (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}) - - min_time, max_time = time(min_time_hour, min_time_mn), time(max_time_hour, max_time_mn) - 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 - - -# Define CRUD operation for the comments - -def get_comments(place: str, page: int, db: Session): - """ Get the 10 last comments for the given place """ - if page == 0: - comments = db.query(models.Comments).order_by(models.Comments.date.desc(), models.Comments.id.desc()).all() - else: - comments = db.query( - models.Comments).filter( - models.Comments.place == place).order_by( - models.Comments.date.desc(), - models.Comments.id.desc()).slice( - (page - - 1) * - 10, - page * - 10).all() - return comments - - -def create_comment(place: str, new_comments: schemas.CommentBase, db: Session): - """ Add a new comment to the database """ - date = datetime.now(tz=pytz.timezone("Europe/Paris")) - db_comment = models.Comments(**new_comments.dict(), date=date, place=place) - db.add(db_comment) - db.commit() - db.refresh(db_comment) - return db_comment - - -def delete_comment(id: int, db: Session): - """ Delete the comment with the matching id """ - if id == 0: - db.query(models.Comments).delete() - else: - db.query(models.Comments).filter(models.Comments.id == id).delete() - db.commit() - - -# Define CRUD operation for the news - -def get_news(place: str, db: Session): - """ Get the news for the given place """ - news = db.query(models.News).filter(models.News.place == place).order_by(models.News.date.desc()).all() - return news - - -def create_news(new_news: schemas.NewsBase, db: Session): - """ Add a news to the database """ - date = datetime.now(tz=pytz.timezone("Europe/Paris")) - db_news = models.News(**new_news.dict(), published_at=date) - db.add(db_news) - db.commit() - db.refresh(db_news) - return db_news - - -def delete_news(id: int, db: Session): - """ Delete the news with the matching id """ - if id == 0: - db.query(models.News).delete() - else: - db.query(models.News).filter(models.News.id == id).delete() - db.commit() +""" +Module to interact with the database +""" +from datetime import date, datetime, time, timedelta +from sqlalchemy.orm import Session +from sqlalchemy.sql import func +import pytz + +from db import models, schemas + + +# Define CRUD operation to collect the statistics + +def get_waiting_time(place: str, db: Session): + """ 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() + return db_record.waiting_time + + +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): + """ Get the average waiting time for each interval between two time steps """ + + 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.weekday(models.Records.date) == weekday, + (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}) + + min_time, max_time = time(min_time_hour, min_time_mn), time(max_time_hour, max_time_mn) + 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 + + +# Define CRUD operation for the comments + +def get_comments(place: str, page: int, db: Session): + """ Get the 10 last comments for the given place """ + if page == 0: + comments = db.query(models.Comments).order_by(models.Comments.date.desc(), models.Comments.id.desc()).all() + else: + comments = db.query( + models.Comments).filter( + models.Comments.place == place).order_by( + models.Comments.date.desc(), + models.Comments.id.desc()).slice( + (page - + 1) * + 10, + page * + 10).all() + return comments + + +def create_comment(place: str, new_comments: schemas.CommentBase, db: Session): + """ Add a new comment to the database """ + date = datetime.now(tz=pytz.timezone("Europe/Paris")) + db_comment = models.Comments(**new_comments.dict(), date=date, place=place) + db.add(db_comment) + db.commit() + db.refresh(db_comment) + return db_comment + + +def delete_comment(id: int, db: Session): + """ Delete the comment with the matching id """ + if id == 0: + db.query(models.Comments).delete() + else: + db.query(models.Comments).filter(models.Comments.id == id).delete() + db.commit() + + +# Define CRUD operation for the news + +def get_news(place: str, db: Session): + """ Get the news for the given place """ + news = db.query(models.News).filter(models.News.place == place).order_by(models.News.date.desc()).all() + return news + + +def create_news(new_news: schemas.NewsBase, db: Session): + """ Add a news to the database """ + date = datetime.now(tz=pytz.timezone("Europe/Paris")) + db_news = models.News(**new_news.dict(), published_at=date) + db.add(db_news) + db.commit() + db.refresh(db_news) + return db_news + + +def delete_news(id: int, db: Session): + """ Delete the news with the matching id """ + if id == 0: + db.query(models.News).delete() + else: + db.query(models.News).filter(models.News.id == id).delete() + db.commit() diff --git a/backend/routers/comments.py b/backend/routers/comments.py index 35808993272cc1b7290d93bfd1bc55f2ae6fdf55..2a88bbc5b18ff8ced7d0534757d614ead4d642c1 100644 --- a/backend/routers/comments.py +++ b/backend/routers/comments.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Body, Depends from sqlalchemy.orm import Session from typing import List @@ -15,7 +15,7 @@ async def get_comments(place: str, page: int = 1, db: Session = Depends(get_db)) @router.post('/{place}/comments', response_model=schemas.Comment) -async def create_comment(place: str, comment: schemas.CommentBase, db: Session = Depends(get_db)): +async def create_comment(place: str, comment: schemas.CommentBase = Body(...), db: Session = Depends(get_db)): return crud.create_comment(place, comment, db) diff --git a/frontend/src/components/Comments.js b/frontend/src/components/Comments.js index 638e600ace69a7ff53eb7d2945abc8b1a4506302..47d1098afe699b8ba9940c8854c40ba754643226 100644 --- a/frontend/src/components/Comments.js +++ b/frontend/src/components/Comments.js @@ -1,8 +1,41 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import axios from "axios"; +import "../styles/Comments.css"; + export default function Comments({ place }) { const [comments, setComments] = useState([]); + const [newComment, setNewComment] = useState(""); + const input = useRef(); + const chat = useRef(); + + const Submit = (ev) => { + if (newComment.replace(/\s/g, "").length) { + ev.preventDefault(); + axios + .post(`${process.env.REACT_APP_BASE_URL_BACK}/${place}/comments`, { + comment: newComment, + }) + .then((res) => { + let update = comments.map((_, index) => (index ? comments[index - 1] : res.data)); + update.push(comments[comments.length - 1]); + setComments(update); + updateValue(""); + }) + .catch((e) => { + console.log(e); + updateValue(""); + }); + } + }; + + const updateValue = (value) => { + setNewComment(value); + if (input.current) { + input.current.style.height = ""; + input.current.style.height = `${input.current.scrollHeight + 5}px`; + } + }; useEffect(() => { axios @@ -11,16 +44,41 @@ export default function Comments({ place }) { setComments(res.data); }) .catch((e) => console.log(e)); - }); + }, []); + + useEffect(() => { + if (chat.current) { + let position = chat.current.scrollHeight - chat.current.clientHeight; + chat.current.scrollTop = position; + } + }, [chat.current]); return ( - <div style={{ width: "25%", overflowY: "scroll" }}> - {comments.map((comment, index) => ( - <div key={index} style={{ display: "flex", flexDirection: "column", border: "solid black 1px", borderRadius: "0.5rem", margin: "1rem", padding: "0.5rem" }}> - <div style={{marginBottom: "0.5rem", textAlign: "left"}}>{comment.comment}</div> - <div style={{fontSize: "0.7rem", alignSelf: "flex-start", marginLeft: "0.2rem"}}>{comment.date.split("T").reduce((date, hours) => `À ${hours.substring(0,5)} le ${date}`)}</div> - </div> - ))} + <div id="comments-side-bar"> + <div ref={chat} id="comments-scroll-bar"> + {comments.map((comment, index) => ( + <div key={index} className="comment"> + <div className="comment-content">{comment.comment}</div> + <div className="comment-date"> + {comment.date + .split("T") + .reduce((date, hours) => `À ${hours.substring(0, 5)} le ${date}`)} + </div> + </div> + ))} + </div> + <div id="comment-input-container"> + <textarea + id="comments-input" + ref={input} + value={newComment} + onChange={(ev) => updateValue(ev.target.value)} + placeholder="Ajouter un commentaire..." + /> + <button id="comment-input-button" onClick={Submit}> + Envoyer + </button> + </div> </div> ); } diff --git a/frontend/src/styles/Comments.css b/frontend/src/styles/Comments.css new file mode 100644 index 0000000000000000000000000000000000000000..18e9383c5313f13ed76fa2d658dc0275cae5f8a1 --- /dev/null +++ b/frontend/src/styles/Comments.css @@ -0,0 +1,64 @@ +#comments-input { + resize: none; + display : block; + height: 2.4rem; + width: 100%; + max-height: 10rem; + overflow-x: hidden; + overflow-y: auto; + padding: 0.2rem; + border-radius: 0.5rem; +} + +#comments-side-bar { + width: 25%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#comments-scroll-bar { + flex: 1; + display: flex; + flex-direction: column-reverse; + overflow-y: scroll; +} + +.comment { + display: flex; + flex-direction: column; + border: 1px solid black; + border-radius: 0.5rem; + margin: 1rem; + padding: 0.5rem; +} + +.comment-content { + margin-bottom: 0.5rem; + text-align: left; + word-wrap: break-word; +} + +.comment-date { + font-size: 0.7rem; + align-self: flex-start; + margin-left: 0.2rem; +} + +#comment-input-container { + display: flex; + width: 100%; + background-color: rgb(59, 137, 255); +} + +#comment-input-button { + background-color: rgb(17, 2, 145); + color: white; + height: 2.4rem; + border-radius: 0.5rem; + padding: 0.2rem; +} + +#comment-input-button:hover { + background-color: rgb(20, 0, 196); +} \ No newline at end of file diff --git a/frontend/src/styles/eiffel.css b/frontend/src/styles/eiffel.css index 46fd353b1266682dabdd59bf3b3adbc1ce93ac83..fe269d398b90444dc9b7636ce5847c9cc4c1a966 100644 --- a/frontend/src/styles/eiffel.css +++ b/frontend/src/styles/eiffel.css @@ -1,11 +1,12 @@ .eiffel-container { display: flex; height: 100%; + justify-content: space-between; } #eiffel-main-page { - width: 75%; + width: 70%; flex-direction: column; - justify-content: space-between; align-content: center; + padding-left: 2rem } \ No newline at end of file