Compare commits

..

4 Commits

Author SHA1 Message Date
Lilian
8c83658708 rm db 2023-02-28 09:24:48 +01:00
2dc81bb4e3 cette fois on est bon 2023-02-26 17:12:10 +01:00
71038c169c Prêt a déployer 2023-02-26 16:29:05 +01:00
Lilian
9393c88f8e Salles 2023-02-26 11:37:56 +01:00
51 changed files with 2031 additions and 1486 deletions

View File

@ -5,7 +5,7 @@ from typing import List
from fastapi import Depends, HTTPException, status, Query from fastapi import Depends, HTTPException, status, Query
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import func from sqlalchemy import func
from sqlmodel import Session, delete, select, col from sqlmodel import Session, delete, select, col, update
from database.auth.crud import get_user_from_token from database.auth.crud import get_user_from_token
from database.auth.models import User from database.auth.models import User
@ -59,9 +59,15 @@ def change_room_status(room: Room, public: bool, db: Session):
return room return room
def delete_room_db(room: Room, db: Session):
db.delete(room)
db.commit()
return True
def get_member_from_user(user_id: int, room_id: int, db: Session): def get_member_from_user(user_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id == member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.user_id == user_id)).first() room_id, Member.user_id == user_id)).first()
return member return member
@ -77,7 +83,7 @@ def get_member_from_token(token: str, room_id: int, db: Session):
def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session): def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id == member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.anonymous_id == anonymous_id)).first() room_id, Member.anonymous_id == anonymous_id)).first()
return member return member
@ -109,7 +115,8 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
return member return member
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session): def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False,
db: Session):
member_id = generate_unique_code(Member, s=db) member_id = generate_unique_code(Member, s=db)
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting, member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
id_code=member_id) id_code=member_id)
@ -154,7 +161,7 @@ def disconnect_member(member: Member, db: Session):
return member return member
def validate_username(username: str, room: Room, db: Session = Depends(get_session)): def validate_username(username: str, room: Room, db: Session = Depends(get_session)):
if len(username) > 20: if len(username) > 20:
return None return None
members = select(Member.anonymous_id).where( members = select(Member.anonymous_id).where(
@ -269,6 +276,8 @@ def refuse_waiter(member: Member, db: Session):
def leave_room(member: Member, db: Session): def leave_room(member: Member, db: Session):
# db.execute(delete(Challenger).where(col(Challenger.member_id) == member.id))
# db.execute(delete(TmpCorrection).where(col(TmpCorrection.member_id) == member.id))
db.delete(member) db.delete(member)
db.commit() db.commit()
return None return None
@ -324,6 +333,12 @@ def getChallenges(c: Challenger, db: Session):
return challenges return challenges
def getMemberChallenges(m: Member, p: Parcours, db: Session):
challenges = db.exec(select(Challenge).where(Challenge.challenger_mid == m.id,
Challenge.challenger_pid == p.id)).all()
return challenges
def getTops(p: Parcours, db: Session): def getTops(p: Parcours, db: Session):
tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by( tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by(
col(Challenge.mistakes), col(Challenge.time)).limit(3)).all() col(Challenge.mistakes), col(Challenge.time)).limit(3)).all()
@ -370,6 +385,13 @@ def getMemberAvgRank(m: Member, p: Parcours, db: Session):
return getAvgRank(challenger, p, db) return getAvgRank(challenger, p, db)
def getMemberValidated(m: Member, p: Parcours, db: Session):
challenger = db.exec(select(Challenger).where(Challenger.member_id == m.id)).first()
if challenger is None or challenger.validated is None:
return None
return challenger.validated
def serialize_parcours(parcours: Parcours, member: Member, db: Session): def serialize_parcours(parcours: Parcours, member: Member, db: Session):
tops = getTops(parcours, db) tops = getTops(parcours, db)
avgTop = getAvgTops(parcours, db) avgTop = getAvgTops(parcours, db)
@ -392,55 +414,13 @@ def serialize_parcours(parcours: Parcours, member: Member, db: Session):
challengers = db.exec(statement).all() challengers = db.exec(statement).all()
challs = {c.member.id_code: { challs = {c.member.id_code: {
"challenger": {"id_code": c.member.id_code, "name": getUsername(c.member)}, "challenger": {"id_code": c.member.id_code, "name": getUsername(c.member), "validated": c.validated},
# 'validated': chall.mistakes <= parcours.max_mistakes # 'validated': chall.mistakes <= parcours.max_mistakes
"challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)] "challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)]
} for c in challengers} } for c in challengers}
return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank, return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank,
"validated": challenger.validated if challenger != None else False, "ranking": avgTop} "validated": challenger.validated if challenger != None else False, "ranking": avgTop}
tops = []
challs = {}
challenges = sorted(parcours.challenges, key=lambda x: (
x.note['value'], x.time), reverse=True)
memberRank = None
rank = None
pb = None
validated = False
total = 0
for i, chall in enumerate(challenges):
total += chall.note['value']
id = chall.challenger.id_code
name = chall.challenger.user.username if chall.challenger.user_id != None else chall.challenger.anonymous.username
if i <= 2:
tops.append({"challenger": {"id_code": id, "name": name},
"note": chall.note, "time": chall.time})
if id == member.id_code:
if challs.get(id) is None:
rank = i + 1
memberRank = len(challs) + 1
pb = {"note": chall.note, "time": chall.time}
if validated is False and chall.validated:
validated = True
if member.is_admin or chall.challenger.id_code == member.id_code:
t = challs.get(id, {"total": 0})['total']
challs[id] = {"challenger": {"id_code": id, "name": name
}, "challenges": [*challs.get(id, {'challenges': []})['challenges'],
Challenges(
**{**chall.dict(), "canCorrige": chall.data != []})],
"total": t + chall.note['value']}
topMembers = [{**c['challenger'], "avg": c['total'] /
len(c['challenges'])} for id, c in challs.items()]
topMembers.sort(key=lambda x: x['avg'], reverse=True)
return {**parcours.dict(), "tops": tops, "challenges": challs, "rank": rank, "memberRank": memberRank, "pb": pb,
"validated": validated,
'avg': None if len(parcours.challenges) == 0 else round(total / len(parcours.challenges), 2),
"ranking": topMembers}
def change_anonymous_clientId(anonymous: Anonymous, db: Session): def change_anonymous_clientId(anonymous: Anonymous, db: Session):
@ -510,29 +490,23 @@ def deleteParcoursRelated(parcours: Parcours, db: Session):
db.commit() db.commit()
def change_challengers_validation(p: Parcours, validation: int, db: Session): def change_challengers_validation(p: Parcours, db: Session):
challengers = db.exec(select(Challenger).where( stmt = update(Challenger).values(
Challenger.parcours_id == p.id)).all() validated=select(Challenge.id).where(Challenge.challenger_mid == Challenger.member_id,
challs = [] Challenge.challenger_pid == Challenger.parcours_id,
for c in challengers: Challenge.validated == 1).exists()).where(
validated = c.best <= validation Challenger.parcours_id == p.id)
if validated != c.validated: db.execute(stmt)
c.validated = validated
challs.append(c)
db.bulk_save_objects(challs)
db.commit() db.commit()
return
def change_challenges_validation(p: Parcours, validation: int, db: Session): def change_challenges_validation(p: Parcours, db: Session):
challenges = db.exec(select(Challenge).where( challenges = db.exec(select(Challenge).where(
Challenge.parcours_id == p.id_code)).all() Challenge.parcours_id == p.id_code)).all()
print('CHALLS', challenges)
challs = [] challs = []
for c in challenges: for c in challenges:
validated = c.mistakes <= validation validated = c.time <= p.time * 60 and c.mistakes <= p.max_mistakes
print('CHAL', validated, c.validated, c)
if validated != c.validated: if validated != c.validated:
c.validated = validated c.validated = validated
challs.append(c) challs.append(c)
@ -541,9 +515,10 @@ def change_challenges_validation(p: Parcours, validation: int, db: Session):
db.commit() db.commit()
def changeValidation(p: Parcours, validation: int, db: Session): def changeValidation(p: Parcours, db: Session):
change_challengers_validation(p, validation, db) change_challenges_validation(p, db)
change_challenges_validation(p, validation, db)
change_challengers_validation(p, db)
def compareExercices(old: list[Exercices], new: list[ExercicesCreate]): def compareExercices(old: list[Exercices], new: list[ExercicesCreate]):
@ -563,9 +538,7 @@ def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Ses
update_challenges = True update_challenges = True
parcours_obj.exercices = exercices parcours_obj.exercices = exercices
if parcours_obj.max_mistakes != parcours.max_mistakes: update_validated = parcours_obj.max_mistakes != parcours.max_mistakes or parcours_obj.time != parcours.time
changeValidation(parcours_obj, parcours.max_mistakes, db)
parcours_obj.name = parcours.name parcours_obj.name = parcours.name
parcours_obj.time = parcours.time parcours_obj.time = parcours.time
parcours_obj.max_mistakes = parcours.max_mistakes parcours_obj.max_mistakes = parcours.max_mistakes
@ -574,6 +547,8 @@ def update_parcours_db(parcours: ParcoursCreate, parcours_obj: Parcours, db: Ses
db.commit() db.commit()
db.refresh(parcours_obj) db.refresh(parcours_obj)
if update_validated:
changeValidation(parcours_obj, db)
return parcours_obj, update_challenges return parcours_obj, update_challenges
@ -728,7 +703,9 @@ def checkValidated(challenger: Challenger, db: Session, challenge: Challenge | N
def create_challenge(data: List[CorrigedData], challenger: Member, parcours: Parcours, time: int, mistakes: int, def create_challenge(data: List[CorrigedData], challenger: Member, parcours: Parcours, time: int, mistakes: int,
isCorriged: bool, db: Session): isCorriged: bool, db: Session):
challenger_obj: Challenger = getChallenger(parcours, challenger, db) challenger_obj: Challenger = getChallenger(parcours, challenger, db)
validated = mistakes <= parcours.max_mistakes print('VALIDATING', time <= parcours.time * 60 and mistakes <= parcours.max_mistakes, time, parcours.time)
validated = time <= parcours.time * 60 and mistakes <= parcours.max_mistakes
challenge = Challenge(data=data, challenger_pid=challenger_obj.parcours_id, challenger_mid=challenger_obj.member_id, challenge = Challenge(data=data, challenger_pid=challenger_obj.parcours_id, challenger_mid=challenger_obj.member_id,
parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged, parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged,
id_code=generate_unique_code(Challenge, s=db), validated=validated) id_code=generate_unique_code(Challenge, s=db), validated=validated)
@ -767,7 +744,7 @@ def change_challenge(challenge: Challenge, corriged: CorrigedChallenge, db: Sess
challenger.best = corriged['mistakes'] challenger.best = corriged['mistakes']
challenger.best_time = challenge.time challenger.best_time = challenge.time
validated = corriged['mistakes'] <= parcours.max_mistakes validated = challenge.time <= parcours.time * 60 and corriged['mistakes'] <= parcours.max_mistakes
challenge.validated = validated challenge.validated = validated
if challenger.validated == False and validated: if challenger.validated == False and validated:
@ -833,7 +810,7 @@ def check_admin(member: Member = Depends(get_member_dep)):
def get_parcours(parcours_id: str, room: Room = Depends(get_room), db: Session = Depends(get_session)): def get_parcours(parcours_id: str, room: Room = Depends(get_room), db: Session = Depends(get_session)):
room = db.exec(select(Parcours).where(Parcours.id_code == room = db.exec(select(Parcours).where(Parcours.id_code ==
parcours_id, Parcours.room_id == room.id)).first() parcours_id, Parcours.room_id == room.id)).first()
if room is None: if room is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Parcours introuvable") status_code=status.HTTP_404_NOT_FOUND, detail="Parcours introuvable")
@ -844,10 +821,12 @@ def get_exercices(parcours: Parcours = Depends(get_parcours), db: Session = Depe
exercices = db.exec(select(Exercice).where(col(Exercice.id_code).in_( exercices = db.exec(select(Exercice).where(col(Exercice.id_code).in_(
[e['exercice_id'] for e in parcours.exercices]))).all() [e['exercice_id'] for e in parcours.exercices]))).all()
return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']} for e in exercices] return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']}
for e in exercices]
def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep), db: Session = Depends(get_session)): def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep),
db: Session = Depends(get_session)):
tmpCorr = db.exec(select(TmpCorrection).where( tmpCorr = db.exec(select(TmpCorrection).where(
TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first() TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first()
if tmpCorr is None: if tmpCorr is None:

View File

@ -24,8 +24,8 @@ class Room(RoomBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
id_code: str = Field(index=True) id_code: str = Field(index=True)
members: List['Member'] = Relationship(back_populates="room") members: List['Member'] = Relationship(back_populates="room",sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
parcours: List['Parcours'] = Relationship(back_populates="room") parcours: List['Parcours'] = Relationship(back_populates="room",sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
class AnonymousBase(SQLModel): class AnonymousBase(SQLModel):
@ -58,7 +58,9 @@ class Member(SQLModel, table=True):
room_id: int = Field(foreign_key="room.id") room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='members') room: Room = Relationship(back_populates='members')
challengers: List["Challenger"] = Relationship(back_populates="member") challengers: List["Challenger"] = Relationship(back_populates="member", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
corrections: List["TmpCorrection"] = Relationship(back_populates="member", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
is_admin: bool = False is_admin: bool = False
@ -68,7 +70,6 @@ class Member(SQLModel, table=True):
waiter_code: Optional[str] = Field(default=None) waiter_code: Optional[str] = Field(default=None)
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
class ExercicesCreate(SQLModel): class ExercicesCreate(SQLModel):
@ -101,7 +102,7 @@ class Parcours(SQLModel, table=True):
room_id: int = Field(foreign_key="room.id") room_id: int = Field(foreign_key="room.id")
room: Room = Relationship(back_populates='parcours') room: Room = Relationship(back_populates='parcours')
challengers: list[Challenger] = Relationship(back_populates="parcours") challengers: list[Challenger] = Relationship(back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
name: str name: str
time: int time: int
@ -110,9 +111,9 @@ class Parcours(SQLModel, table=True):
max_mistakes: int max_mistakes: int
exercices: List[Exercices] = Field(sa_column=Column(JSON)) exercices: List[Exercices] = Field(sa_column=Column(JSON))
challenges: List["Challenge"] = Relationship(back_populates="parcours") challenges: List["Challenge"] = Relationship(back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
corrections: List["TmpCorrection"] = Relationship( corrections: List["TmpCorrection"] = Relationship(
back_populates="parcours") back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
@ -144,6 +145,7 @@ class ParcoursReadUpdate(SQLModel):
class ChallengerInfo(BaseModel): class ChallengerInfo(BaseModel):
name: str name: str
id_code: str id_code: str
validated: bool = False
class ChallengerAverage(ChallengerInfo): class ChallengerAverage(ChallengerInfo):

Binary file not shown.

View File

@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, Query, UploadFile, HTTPException, status
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from fastapi_pagination.ext.sqlalchemy_future import paginate as p from fastapi_pagination.ext.sqlalchemy_future import paginate as p
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import Session, select from sqlmodel import Session, select, col
from database.auth.models import User from database.auth.models import User
from database.db import get_session from database.db import get_session
@ -85,6 +85,8 @@ def get_user_exercices(user: User = Depends(get_current_user),
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where( sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
ExercicesTagLink.tag_id == t).exists() ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub) statement = statement.where(sub)
statement = statement.order_by(col(Exercice.id).desc())
page = p(db, statement) page = p(db, statement)
exercices = page.items exercices = page.items
page.items = [ page.items = [
@ -116,7 +118,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where( sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id == Exercice.id).where(
ExercicesTagLink.tag_id == t).exists() ExercicesTagLink.tag_id == t).exists()
statement = statement.where(sub) statement = statement.where(sub)
statement = statement.order_by(col(Exercice.id).desc())
page = p(db, statement) page = p(db, statement)
print('¨PAGE', page) print('¨PAGE', page)
exercices = page.items exercices = page.items

View File

@ -8,7 +8,7 @@ from database.auth.crud import get_user_from_token
from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \ from database.room.crud import change_room_name, change_room_status, serialize_member, check_user_in_room, \
create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \ create_anonymous, create_member, get_member, get_member_from_token, get_member_from_reconnect_code, connect_member, \
disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter disconnect_member, get_waiter, accept_waiter, leave_room, refuse_waiter
from database.room.models import Room, Member, MemberRead, Waiter from database.room.models import Room, Member, MemberRead, Waiter, Challenger
from services.websocket import Consumer from services.websocket import Consumer
if TYPE_CHECKING: if TYPE_CHECKING:
@ -17,7 +17,7 @@ if TYPE_CHECKING:
class RoomConsumer(Consumer): class RoomConsumer(Consumer):
def __init__(self, ws: WebSocket, room: Room, manager: "RoomManager", db: Session): def __init__(self, ws: WebSocket, room: Room | None, manager: "RoomManager", db: Session):
self.room = room self.room = room
self.ws = ws self.ws = ws
self.manager = manager self.manager = manager
@ -25,14 +25,22 @@ class RoomConsumer(Consumer):
self.member = None self.member = None
self.banned = False self.banned = False
async def connect(self):
await self.ws.accept()
if self.room is None:
await self.send_error("Salle introuvable", code=404)
await self.ws.close()
return False
return True
# WS Utilities # WS Utilities
async def send(self, payload: Any | Callable): async def send(self, payload: Any | Callable):
if callable(payload): if callable(payload):
payload = payload(self.member) payload = payload(self.member)
return await super().send(payload) return await super().send(payload)
async def connect(self):
await self.ws.accept()
async def direct_send(self, type: str, payload: Any, code: int | None = None): async def direct_send(self, type: str, payload: Any, code: int | None = None):
sending = {'type': type, "data": payload, } sending = {'type': type, "data": payload, }
@ -243,12 +251,13 @@ class RoomConsumer(Consumer):
@Consumer.event('leave', conditions=[isMember]) @Consumer.event('leave', conditions=[isMember])
async def leave(self): async def leave(self):
print('LEAVED', self.member, isinstance(self.member, Member), isinstance(self.member, Challenger))
if self.member.is_admin is True: if self.member.is_admin is True:
await self.send_error("Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur") await self.send_error("Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur")
return return
member_obj = serialize_member(self.member) member_obj = serialize_member(self.member)
leave_room(self.member, self.db) leave_room(self.member, self.db)
self.member = None
await self.direct_send(type="successfully_leaved", payload={}) await self.direct_send(type="successfully_leaved", payload={})
await self.broadcast(type='leaved', payload={"member": member_obj}) await self.broadcast(type='leaved', payload={"member": member_obj})
self.member = None self.member = None
@ -294,13 +303,13 @@ class RoomConsumer(Consumer):
self.manager.remove(self.room.id, self) self.manager.remove(self.room.id, self)
return {"waiter_id": waiter_id} return {"waiter_id": waiter_id}
@Consumer.sending("banned", conditions=[isMember]) # @Consumer.sending("banned", conditions=[isMember])
async def banned(self): # def banned(self):
self.member = None # self.member = None
self.manager.remove(self.room.id_code, self) # self.manager.remove(self.room.id_code, self)
self.banned = True # self.banned = True
#await self.ws.close() # #await self.ws.close()
return {} # return {}
@Consumer.sending('ping', conditions=[isMember]) @Consumer.sending('ping', conditions=[isMember])
def ping(self): def ping(self):

View File

@ -39,7 +39,6 @@ class RoomManager:
if group in self.active_connections: if group in self.active_connections:
for connection in list(set(self.active_connections[group])): for connection in list(set(self.active_connections[group])):
print(connection, connection.ws.state, connection.ws.client_state, connection.ws.application_state)
if connection not in exclude and all(f(connection) for f in conditions): if connection not in exclude and all(f(connection) for f in conditions):
await self._send(connection, message, group) await self._send(connection, message, group)

View File

@ -8,35 +8,12 @@ from sqlmodel import Session, select
from database.auth.models import User from database.auth.models import User
from database.db import get_session from database.db import get_session
from database.exercices.models import Exercice from database.exercices.models import Exercice
from database.room.crud import delete_room_db, getUsername, getMemberValidated
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \ from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \ create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \ update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \ create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank, getMemberChallenges
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
from generateur.generateur_main import generate_from_path, parseGeneratorOut
from routes.room.consumer import RoomConsumer
from routes.room.manager import RoomManager
from services.auth import get_current_user_optional
from services.io import add_fast_api_root
from services.misc import stripKeyDict
from typing import List, Optional
from fastapi import APIRouter, Depends, WebSocket, status, Query, Body
from fastapi.exceptions import HTTPException
from pydantic import BaseModel
from sqlmodel import Session, select
from database.auth.models import User
from database.db import get_session
from database.exercices.models import Exercice
from database.room.crud import serialize_parcours_short, change_correction, corrige_challenge, \
create_parcours_db, delete_parcours_db, create_room_db, get_member_dep, check_room, serialize_room, \
update_parcours_db, get_parcours, get_room, check_admin, get_exercices, get_challenge, get_correction, \
create_tmp_correction, create_challenge, change_challenge, serialize_parcours, getTops, getAvgRank, getRank, \
getAvgTops, ChallengerFromChallenge, getMemberAvgRank, getMemberRank
from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \ from database.room.models import Challenge, ChallengeRead, Challenges, ParcoursReadUpdate, ChallengeInfo, Member, \
Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \ Parcours, ParcoursCreate, ParcoursRead, ParcoursReadShort, Room, RoomConnectionInfos, \
RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData RoomCreate, RoomInfo, TmpCorrection, CorrigedData, CorrectionData
@ -68,6 +45,14 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
return serialize_room(room, member, db) return serialize_room(room, member, db)
@router.delete('/room/{room_id}', dependencies=[Depends(check_admin)])
async def delete_room(room: Room = Depends(get_room), m: RoomManager = Depends(get_manager),
db: Session = Depends(get_session)):
delete_room_db(room, db)
await m.broadcast({"type": "deleted"}, room.id_code)
return {"message": "ok"}
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead) @router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin), async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin),
m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)): m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
@ -88,6 +73,8 @@ async def get_parcours_route(*, parcours: Parcours = Depends(get_parcours), memb
return serialize_parcours(parcours, member, db) return serialize_parcours(parcours, member, db)
@router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead) @router.put('/room/{room_id}/parcours/{parcours_id}', response_model=ParcoursRead)
async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin), async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Member = Depends(check_admin),
parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager), parcours_old: Parcours = Depends(get_parcours), m: RoomManager = Depends(get_manager),
@ -103,9 +90,16 @@ async def update_parcours(*, room_id: str, parcours: ParcoursCreate, member: Mem
await m.broadcast({"type": "edit_parcours", "data": { await m.broadcast({"type": "edit_parcours", "data": {
"parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}}, "parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}},
parcours_old.id_code) parcours_old.id_code)
print('BROADCASTING')
await m.broadcast(
lambda m: {"type": "update_challenges", "data": {"challenger": {"id_code": m.id_code, "name": getUsername(m), "validated": getMemberValidated(m, parcours_obj, db)},
"challenges": [Challenges(
**{**chall.dict(), "canCorrige": chall.data != []}).dict() for
chall in
getMemberChallenges(m, parcours_obj, db)]}},
parcours_old.id_code, conditions=[lambda m: m.member.id_code != member.id_code])
return serialize_parcours(parcours_obj, member, db) return serialize_parcours(parcours_obj, member, db)
return {**parcours_obj.dict()}
@router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)]) @router.delete('/room/{room_id}/parcours/{parcours_id}', dependencies=[Depends(check_admin)])
@ -183,7 +177,7 @@ async def send_challenge(*, challenge: List[CorrectionData], correction: TmpCorr
}}, parcours.id_code) }}, parcours.id_code)
print('CHALLENGE', chall) print('CHALLENGE', chall)
db.delete(correction) db.delete(correction)
returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes} returnValue = {**chall.dict()}
db.commit() db.commit()
return returnValue return returnValue
# return {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes} # return {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
@ -258,8 +252,5 @@ async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challen
@router.websocket('/ws/room/{room_id}') @router.websocket('/ws/room/{room_id}')
async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session), async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session),
m: RoomManager = Depends(get_manager)): m: RoomManager = Depends(get_manager)):
if room is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Room not found')
consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db) consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db)
await consumer.run() await consumer.run()

View File

@ -1,9 +1,10 @@
from typing import List, Callable, Any, Dict
from pydantic import validate_arguments, BaseModel
from fastapi.websockets import WebSocketDisconnect, WebSocket
from pydantic.error_wrappers import ValidationError
import inspect import inspect
from starlette.websockets import WebSocketState from typing import List, Callable, Any, Dict
from fastapi.websockets import WebSocketDisconnect, WebSocket
from pydantic import validate_arguments, BaseModel
from pydantic.error_wrappers import ValidationError
def make_event_decorator(eventsDict): def make_event_decorator(eventsDict):
def _(name: str | List, conditions: List[Callable | bool] = []): def _(name: str | List, conditions: List[Callable | bool] = []):
@ -62,7 +63,8 @@ class Consumer:
#self.events: Dict[str, Callable] = {} #self.events: Dict[str, Callable] = {}
async def connect(self): async def connect(self):
pass await self.ws.accept()
return True
async def validation_error_handler(self, e: ValidationError): async def validation_error_handler(self, e: ValidationError):
errors = e.errors() errors = e.errors()
@ -132,7 +134,9 @@ class Consumer:
pass pass
async def run(self): async def run(self):
await self.connect() accepted = await self.connect()
if accepted is False:
return
try: try:
while True: while True:
data = await self.ws.receive_json() data = await self.ws.receive_json()

3
frontend/.gitignore vendored
View File

@ -8,4 +8,5 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
.idea .idea
*/database*

View File

@ -1,17 +1,17 @@
import axios from 'axios'; import axios from "axios";
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from "../utils/utils";
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const authInstance = axios.create({ export const authInstance = axios.create({
baseURL: `${env.API_BASE}`, baseURL: `${env.PUBLIC_API_BASE}`,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
Accept: 'application/json', Accept: "application/json",
'Access-Control-Allow-Origin': '*', "Access-Control-Allow-Origin": "*"
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '', //'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
} }
}); });
authInstance.interceptors.request.use(autoRefresh, (error) => { authInstance.interceptors.request.use(autoRefresh, (error) => {
Promise.reject(error); Promise.reject(error);
}); });

View File

@ -1,10 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { parse, stringify } from 'qs' import { parse, stringify } from 'qs'
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from '../utils/utils';
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const exoInstance = axios.create({ export const exoInstance = axios.create({
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}}, paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
baseURL: `${env.API_BASE}`, baseURL: `${env.PUBLIC_API_BASE}`,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json', Accept: 'application/json',

View File

@ -1,20 +1,20 @@
import axios from 'axios'; import axios from "axios";
import { autoRefresh } from '../utils/utils'; import { autoRefresh } from "../utils/utils";
import {env} from '$env/dynamic/private'; import { env } from "$env/dynamic/public";
export const roomInstance = axios.create({ export const roomInstance = axios.create({
baseURL: `${env.API_BASE}/room`, baseURL: `${env.PUBLIC_API_BASE}room`,
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
Accept: 'application/json', Accept: "application/json",
'Access-Control-Allow-Origin': '*', "Access-Control-Allow-Origin": "*"
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '', //'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
} }
}); });
roomInstance.interceptors.request.use( roomInstance.interceptors.request.use(
autoRefresh, autoRefresh,
(error) => { (error) => {
Promise.reject(error); Promise.reject(error);
} }
); );

View File

@ -1,12 +1,13 @@
/* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */ /* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
} }
.spinner { .spinner {
width: 30px; width: 30px;
height:30px; height: 30px;
border: 3px solid $contrast; border: 3px solid $contrast;
border-bottom-color: transparent; border-bottom-color: transparent;
border-radius: 50%; border-radius: 50%;
@ -14,14 +15,15 @@
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
} }
.italic { .italic {
font-style: italic; font-style: italic;
} }
.underline { .underline {
text-decoration: underline; text-decoration: underline;
} }
@keyframes rotation { @keyframes rotation {
@ -34,95 +36,109 @@
} }
.container { .container {
height: calc(100vh - 100px); // 100% - nav height: calc(100vh - 100px); // 100% - nav
} }
*{ * {
scrollbar-width: auto!important; scrollbar-width: auto !important;
scrollbar-color: $contrast transparent; scrollbar-color: $contrast transparent;
} }
.btn { .btn {
border: none; border: none;
border-radius: 5px; border-radius: 5px;
height: 38px; height: 38px;
font-weight: 700; font-weight: 700;
transition: 0.3s; transition: 0.3s;
margin-bottom: 10px; margin-bottom: 10px;
margin-right: 7px; margin-right: 7px;
padding: 0 50px; padding: 0 50px;
width: max-content; width: max-content;
cursor: pointer; cursor: pointer;
&:disabled{
cursor: not-allowed &:disabled {
} cursor: not-allowed
}
} }
.primary-btn { .primary-btn {
@extend .btn; @extend .btn;
background-color: #fcbf49; background-color: #fcbf49;
&:hover {
background-color: #ac7b19; &:hover {
} background-color: #ac7b19;
}
} }
.danger-btn { .danger-btn {
@extend .btn; @extend .btn;
background-color: #fc5e49; background-color: #fc5e49;
&:hover {
background-color: #ac1919; &:hover {
} background-color: #ac1919;
}
} }
.border-primary-btn { .border-primary-btn {
@extend .btn; @extend .btn;
background-color: transparent; background-color: transparent;
border: 1px solid #fcbf49; border: 1px solid #fcbf49;
color: #fcbf49; color: #fcbf49;
&:hover {
background-color: #fcbf49; &:hover {
color: black; background-color: #fcbf49;
} color: black;
}
} }
.input { .input {
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
padding: 5px 10px; padding: 5px 10px;
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
font-weight: 450; font-weight: 450;
margin: 10px 0; margin: 10px 0;
float: left; float: left;
border: none; border: none;
border-bottom: 1px solid #181553; border-bottom: 1px solid #181553;
transition: 0.3s; transition: 0.3s;
border-radius: 0; border-radius: 0;
margin: 0; margin: 0;
&:focus {
outline: none; &:focus {
border-bottom-color: $contrast; outline: none;
} border-bottom-color: $contrast;
}
} }
.flex-row-center { .flex-row-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
@for $f from 0 through 100 { @for $f from 0 through 100 {
.wp-#{$f} { .wp-#{$f} {
width: 1% * $f; width: 1% * $f;
} }
} }
.sv-dropdown{ .sv-dropdown {
z-index: 10!important; z-index: 10 !important;
} }
.strong{ .strong {
font-weight: 900; font-weight: 900;
} }
.contrast {
color: $contrast;
}
.loading {
cursor: progress;
}

View File

@ -5,7 +5,7 @@
import FaHome from "svelte-icons/fa/FaHome.svelte"; import FaHome from "svelte-icons/fa/FaHome.svelte";
import { afterNavigate } from "$app/navigation"; import { afterNavigate } from "$app/navigation";
import FaUser from "svelte-icons/fa/FaUser.svelte"; import FaUser from "svelte-icons/fa/FaUser.svelte";
import FaSignOutAlt from "svelte-icons/fa/FaSignOutAlt.svelte"; import IoIosLogOut from 'svelte-icons/io/IoIosLogOut.svelte'
const { const {
isAuth, isAuth,
@ -16,43 +16,46 @@
afterNavigate(() => { afterNavigate(() => {
open = false; open = false;
}); });
$: console.log("USERNAME", $username);
</script> </script>
<nav data-sveltekit-preload-data="hover" class:open> <nav data-sveltekit-preload-data="hover" class:open>
<div class="navigate"> <div class="navigate">
<NavLink href="/" exact no_hover class="home"> <NavLink href="/" exact no_hover class="home">
<div class="icon"> <div class="icon">
<FaHome /> <FaHome />
</div> </div>
</NavLink> </NavLink>
<NavLink href="/exercices">Exercices</NavLink> <NavLink href="/exercices">Exercices</NavLink>
<NavLink href="/room">Salles</NavLink> <NavLink href="/room">Salles</NavLink>
</div> </div>
<div class="auth"> <div class="right">
{#if $isAuth && $username != null} <div class="auth">
{#if $isAuth && $username != null}
<NavLink href="/dashboard"> <NavLink href="/dashboard">
<div class="dashboard"> <div class="dashboard">
<div class="icon"> <div class="icon">
<FaUser /> <FaUser />
</div>
{$username}
</div> </div>
{$username}</div> </NavLink>
</NavLink> <div class="icon signout" title="Se déconnecter" on:click={()=>{
<div class="icon signout" title="Se déconnecter" on:click={()=>{
logout() logout()
}}> }}>
<FaSignOutAlt /> <IoIosLogOut />
</div> </div>
{:else} {:else}
<NavLink href="/signup" exact>S'inscrire</NavLink> <NavLink href="/signup" exact>S'inscrire</NavLink>
<NavLink href="/signin" exact>Se connecter</NavLink> <NavLink href="/signin" exact>Se connecter</NavLink>
{/if} {/if}
</div>
<div class="burger" on:click={()=>{open=!open}}><span> </span></div> <div class="burger" on:click={()=>{open=!open}}><span> </span></div>
</div> </div>
</nav> </nav>
<style lang="scss"> <style lang="scss">
@import "../mixins"; @import "../mixins";
@ -107,9 +110,20 @@
} }
} }
.auth {
transition: .3s;
display: flex;
gap: 7px;
@include down(666) {
//display: none;
opacity: 0;
}
}
.burger { .burger {
background: 0 0; background: 0 0;
@include up(750px) { @include up(666) {
display: none; display: none;
} }
border: none; border: none;
@ -164,15 +178,11 @@
.open { .open {
@include down(750px) { @include down(666px) {
.navigate { .navigate {
*:first-child {
display: none
}
max-height: 1000000px; max-height: 1000000px;
// Remove home icon
transition: .2s;
background: rgba($background-dark, 0.8); background: rgba($background-dark, 0.8);
height: 100%; height: 100%;
position: fixed; position: fixed;
@ -184,18 +194,31 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 42px; //gap: 42px;
z-index: 100; z-index: 600;
animation: open .1s ease-in-out forwards;
}
:global(.home){
display: none;
}
.right {
justify-content: end;
width: 100%;
z-index: 601;
display: flex;
} }
.auth { .auth {
justify-content: end; display: flex;
width: 100%; opacity: 1;
z-index: 101;
} }
& .burger { & .burger {
z-index: 1000;
& span::before { & span::before {
bottom: 0; bottom: 0;
@ -220,4 +243,14 @@
padding-right: 20px; padding-right: 20px;
} }
@keyframes open {
0% {
gap: 10px;
opacity: .3;
}
100% {
gap: 42px;
opacity: 1;
}
}
</style> </style>

View File

@ -1,46 +1,51 @@
<script lang="ts"> <script lang="ts">
import {field, form} from "svelte-forms"; import { field, form } from "svelte-forms";
import {max, min, required, email} from "svelte-forms/validators"; import { max, min, required, email } from "svelte-forms/validators";
import LabeledInput from "../forms/LabeledInput.svelte"; import LabeledInput from "../forms/LabeledInput.svelte";
import type {User} from "../../types/auth.type"; import type { User } from "../../types/auth.type";
import {onMount} from "svelte"; import { onMount } from "svelte";
import {errorMsg} from "../../utils/forms.js"; import { errorMsg } from "../../utils/forms.js";
export let user: User export let user: User;
export let myForm; export let myForm;
const username = field('username', user.username, [required(), max(20), min(2)], { const username = field("username", user.username, [required(), max(20), min(2)], {
checkOnInit: true checkOnInit: true
}); });
const name = field('name', user.name || "", [max(50)], { const name = field("name", user.name || "", [max(50)], {
checkOnInit: true checkOnInit: true
}); });
const firstname = field('firstname', user.firstname || "", [max(50),], { const firstname = field("firstname", user.firstname || "", [max(50)], {
checkOnInit: true checkOnInit: true
}); });
const emailField = field('email', user.email || "", [ /*email()*/], { const emailField = field("email", user.email || "", [ /*email()*/], {
checkOnInit: true checkOnInit: true
}); });
onMount(() => { onMount(() => {
myForm = form(username, name, firstname, emailField); myForm = form(username, name, firstname, emailField);
}) });
</script> </script>
{#if !!$myForm} {#if !!$myForm}
<div class=""> <div class="">
<LabeledInput bind:value={$username.value} label="Nom d'utilisateur" type="text" placeholder="Nom d'utilisateur..." <LabeledInput bind:value={$username.value} label="Nom d'utilisateur" type="text" placeholder="Nom d'utilisateur..."
errors={errorMsg($myForm, 'username')}/> errors={errorMsg($myForm, 'username')} />
<LabeledInput bind:value={$emailField.value} label="Email" type="email" placeholder="Email..." <LabeledInput bind:value={$emailField.value} label="Email" type="email" placeholder="Email..."
errors={errorMsg($myForm, 'email')}/> errors={errorMsg($myForm, 'email')} />
<LabeledInput bind:value={$name.value} label="Nom" type="text" placeholder="Nom..." <LabeledInput bind:value={$name.value} label="Nom" type="text" placeholder="Nom..."
errors={errorMsg($myForm, 'name')}/> errors={errorMsg($myForm, 'name')} />
<LabeledInput bind:value={$firstname.value} label="Prénom" type="text" placeholder="Prénom..." <LabeledInput bind:value={$firstname.value} label="Prénom" type="text" placeholder="Prénom..."
errors={errorMsg($myForm, 'firstname')}/> errors={errorMsg($myForm, 'firstname')} />
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
div { div {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@include down(800px){
grid-template-columns: 1fr;
}
} }
</style> </style>

View File

@ -1,31 +1,35 @@
<script lang="ts"> <script lang="ts">
import {field, form} from "svelte-forms"; import { field, form } from "svelte-forms";
import {matchField, min, pattern, required} from "svelte-forms/validators"; import { matchField, min, pattern, required } from "svelte-forms/validators";
import LabeledInput from "../forms/LabeledInput.svelte"; import LabeledInput from "../forms/LabeledInput.svelte";
import {onMount} from "svelte"; import { onMount } from "svelte";
import {errorMsg} from "../../utils/forms"; import { errorMsg } from "../../utils/forms";
const password = field('password', '', [required(), min(8), pattern(/[0-9]/), pattern(/[A-Z]/)], {checkOnInit: true}); const password = field("password", "", [required(), min(8), pattern(/[0-9]/), pattern(/[A-Z]/)], { checkOnInit: true });
const confirm = field('password_confirm', '', [required(), matchField(password)],{checkOnInit: true}); const confirm = field("password_confirm", "", [required(), matchField(password)], { checkOnInit: true });
export let myForm; export let myForm;
onMount(() => { onMount(() => {
myForm = form(password, confirm) myForm = form(password, confirm);
}) });
</script> </script>
{#if !!$myForm} {#if !!$myForm}
<div> <div>
<LabeledInput bind:value={$password.value} type="password" placeholder="Mot de passe..." <LabeledInput bind:value={$password.value} type="password" placeholder="Mot de passe..."
errors={errorMsg($myForm, 'password')}/> errors={errorMsg($myForm, 'password')} />
<LabeledInput bind:value={$confirm.value} type="password" placeholder="Confirmer..." <LabeledInput bind:value={$confirm.value} type="password" placeholder="Confirmer..."
errors={errorMsg($myForm, 'password_confirm')}/> errors={errorMsg($myForm, 'password_confirm')} />
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
div { div {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@include down(800){
grid-template-columns: 1fr;}
} }
</style> </style>

View File

@ -10,7 +10,7 @@
<ul> <ul>
{#each rooms as room} {#each rooms as room}
<li> <li>
<a href="/room/{room.id_code}">{room.name} ({room.admin ? "Administrateur" : "Member"})</a> <a href="/room/{room.id_code}">{room.name} ({room.admin ? "Administrateur" : "Membre"})</a>
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@ -1,34 +1,41 @@
<script lang="ts"> <script lang="ts">
export let icon = null export let icon = null;
export let title; export let title;
export let validate = "Valider !" export let validate = "Valider !";
export let onValidate = null export let onValidate = null;
export let canValid = false export let canValid = false;
</script> </script>
<div> <div>
<h2> <h2>
<div class="icon"> <div class="icon">
<svelte:component this={icon}/></div> <svelte:component this={icon} />
{title}</h2> </div>
<div class="content"> {title}</h2>
<slot/> <div class="content">
<slot />
</div>
{#if !!onValidate}
<div class="btn-container">
<button on:click={onValidate} class="primary-btn" disabled={!canValid}>{validate}</button>
</div> </div>
{#if !!onValidate}
<div class="btn-container">
<button on:click={onValidate} class="primary-btn" disabled={!canValid}>{validate}</button>
</div>
{/if} {/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
h2 { h2 {
display: flex; display: flex;
align-items: center; align-items: center;
@include down(800){
justify-content: center;
}
} }
.icon{
.icon {
margin-right: 10px; margin-right: 10px;
width: 25px; width: 25px;
height: 25px; height: 25px;
@ -49,5 +56,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 10px; margin-top: 10px;
@include down(800){
justify-content: center;
}
} }
</style> </style>

View File

@ -1,71 +1,71 @@
<script lang="ts"> <script lang="ts">
import type { Exercice } from '../../types/exo.type'; import type { Exercice } from "../../types/exo.type";
import { getContext } from 'svelte'; import { getContext } from "svelte";
import ModalCard from './ModalCard.svelte'; import ModalCard from "./ModalCard.svelte";
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import { cloneExo } from '../../requests/exo.request'; import { cloneExo } from "../../requests/exo.request";
import TagContainer from './TagContainer.svelte'; import TagContainer from "./TagContainer.svelte";
import PrivacyIndicator from './PrivacyIndicator.svelte'; import PrivacyIndicator from "./PrivacyIndicator.svelte";
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte'; import MdContentCopy from "svelte-icons/md/MdContentCopy.svelte";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
export let exo: Exercice; export let exo: Exercice;
const { show } = getContext<{ show: Function }>('modal'); const { show } = getContext<{ show: Function }>("modal");
const { navigate } = getContext<{ navigate: Function }>('navigation'); const { navigate } = getContext<{ navigate: Function }>("navigation");
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth'); const { isAuth } = getContext<{ isAuth: Writable<boolean> }>("auth");
const exerciceStore = getContext('exos'); const exerciceStore = getContext("exos");
const tagsStore = getContext('tags'); const tagsStore = getContext("tags");
let opened = false; let opened = false;
const handleClick = () => { const handleClick = () => {
opened = true; opened = true;
navigate(`/exercices/${exo.id_code}`); navigate(`/exercices/${exo.id_code}`);
show( show(
ModalCard, ModalCard,
{ {
exo, exo,
exos: exerciceStore, exos: exerciceStore,
tags: tagsStore tags: tagsStore
}, },
() => { () => {
navigate(-1); navigate(-1);
opened = false; opened = false;
} }
); );
}; };
</script> </script>
<div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}> <div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}>
<h1>{exo.name}</h1> <h1>{exo.name}</h1>
<div class="examples"> <div class="examples">
{#if exo.examples != null} {#if exo.examples != null}
<h2>Exemples</h2> <h2>Exemples</h2>
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if} {#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
{#each exo.examples.data.slice(0, 3) as ex} {#each exo.examples.data.slice(0, 3) as ex}
<p>{ex.calcul}</p> <p>{ex.calcul}</p>
{/each} {/each}
{:else} {:else}
<p>Aucun exemple disponible</p> <p>Aucun exemple disponible</p>
{/if} {/if}
</div> </div>
{#if !!$isAuth && exo.is_author && exo.original == null } {#if !!$isAuth && exo.is_author && exo.original == null }
<div class="status"> <div class="status">
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}> <PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator {exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
> >
</div> </div>
{:else if !exo.is_author} {:else if !exo.is_author}
<div class="status"> <div class="status">
<PrivacyIndicator color={'blue'}> <PrivacyIndicator color={'blue'}>
Par <strong>{exo.author.username}</strong> Par <strong>{exo.author.username}</strong>
</PrivacyIndicator> </PrivacyIndicator>
{#if !!$isAuth} {#if !!$isAuth}
<div <div
data-testid="copy" data-testid="copy"
class="icon" class="icon"
on:keydown={() => {}} on:keydown={() => {}}
on:click|stopPropagation={() => { on:click|stopPropagation={() => {
cloneExo(exo.id_code).then((r) => { cloneExo(exo.id_code).then((r) => {
goto('/exercices/' + r.id_code); goto('/exercices/' + r.id_code);
show(ModalCard, { exo: r }, () => { show(ModalCard, { exo: r }, () => {
@ -73,121 +73,133 @@
}); });
}); });
}} }}
> >
<MdContentCopy /> <MdContentCopy />
</div> </div>
{/if} {/if}
</div> </div>
{:else if exo.is_author && exo.original != null} {:else if exo.is_author && exo.original != null}
<div class="status"> <div class="status">
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator> <PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
</div> </div>
{/if} {/if}
<div class="card-hover" /> <div class="card-hover" />
{#if !!$isAuth} {#if !!$isAuth}
<TagContainer bind:exo /> <TagContainer bind:exo />
{/if} {/if}
<!-- TagContainer Must be directly after card-hover for the hover effect --> <!-- TagContainer Must be directly after card-hover for the hover effect -->
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
* {
transition: 0.45s;
}
.icon {
width: 18px;
height: 18px;
transition: 0.3s;
cursor: pointer;
opacity: 0.7;
transform: scale(0.9);
&:hover {
transform: scale(1);
opacity: 1;
}
}
.status { * {
position: absolute; transition: 0.45s;
top: 0; }
right: 0;
margin: 10px;
z-index: 3;
display: flex;
align-items: center;
gap: 10px;
}
.examples { .icon {
color: gray; width: 18px;
overflow: hidden; height: 18px;
text-overflow: ellipsis; transition: 0.3s;
margin-bottom: 20px; cursor: pointer;
p { opacity: 0.7;
margin: 10px; transform: scale(0.9);
margin-left: 18px;
font-size: 0.95em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
h2 {
font-size: 0.95em;
margin: 10px;
margin-left: 0;
}
}
.card-hover { &:hover {
position: absolute; transform: scale(1);
top: 0; opacity: 1;
right: 0; }
left: 0; }
bottom: 0;
z-index: 1;
border: 1px solid $border;
+ :global(div) {
transition: 0.45s;
}
&:hover {
border: 1px solid $primary;
+ :global(div) {
border: 1px solid $primary;
border-top: none;
}
}
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
}
h1 { .status {
font-size: 1.3em; position: absolute;
margin: 0; top: 0;
position: relative; right: 0;
z-index: 2; margin: 10px;
max-width: 88%; z-index: 3;
display: flex;
align-items: center;
gap: 10px;
}
overflow: hidden; .examples {
text-overflow: ellipsis; color: gray;
display: -webkit-box; overflow: hidden;
-webkit-line-clamp: 2; text-overflow: ellipsis;
-webkit-box-orient: vertical; margin-bottom: 20px;
word-wrap: break-word;
&:hover {
color: $primary;
}
}
.card { p {
border: 1px solid black; margin: 10px;
padding: 20px; margin-left: 18px;
cursor: pointer; font-size: 0.95em;
position: relative; overflow: hidden;
background-color: $background; text-overflow: ellipsis;
min-height: 250px; white-space: nowrap;
max-height: 300px; }
&:hover {
transform: translateX(10px) translateY(-10px); h2 {
} font-size: 0.95em;
} margin: 10px;
margin-left: 0;
}
}
.card-hover {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 1;
border: 1px solid $border;
+ :global(div) {
transition: 0.45s;
}
&:hover {
border: 1px solid $primary;
+ :global(div) {
border: 1px solid $primary;
border-top: none;
}
}
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
}
h1 {
font-size: 1.3em;
margin: 0;
position: relative;
z-index: 2;
max-width: 88%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-wrap: break-word;
&:hover {
color: $primary;
}
}
.card {
border: 1px solid black;
padding: 20px;
cursor: pointer;
position: relative;
background-color: $background;
min-height: 250px;
max-height: 300px;
min-width: 250px;
&:hover {
transform: translateX(10px) translateY(-10px);
}
}
</style> </style>

View File

@ -1,33 +1,43 @@
<script lang="ts"> <script lang="ts">
import type { Exercice, Page } from '../../types/exo.type'; import type { Exercice, Page } from "../../types/exo.type";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
import EditForm from './EditForm.svelte'; import EditForm from "./EditForm.svelte";
export let cancel: Function; export let cancel: Function;
export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>; export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>;
const updateExo = (e: Exercice) => { const updateExo = (e: Exercice) => {
exos.update((o) => { exos.update((o) => {
return { ...o, data: { ...o.data, items: [e, ...o.data.items] } }; return { ...o, data: { ...o.data, items: [e, ...o.data.items] } };
}); });
}; };
</script> </script>
<div> <div>
<h1>Nouvel exercice</h1> <h1>Nouvel exercice</h1>
<EditForm editing={false} {cancel} {updateExo} /> <EditForm editing={false} {cancel} {updateExo} />
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
div { @import '../../mixins';
background: $background;
padding: 50px; div {
display: flex; background: $background;
flex-direction: column; padding: 50px;
gap: 20px; display: flex;
align-items: flex-start; flex-direction: column;
} gap: 20px;
h1 { align-items: flex-start;
font-size: 1.5em; @include down(800){
} width: 100%;
height: 100%;
min-width: 0;
}
min-width: 800px;
}
h1 {
font-size: 1.5em;
}
</style> </style>

View File

@ -86,7 +86,7 @@
{/if} {/if}
<div class="icons"> <div class="icons">
<div> <div>
<div class="icon" style:color="black" on:click={() => close()} on:keypress={() => {}}> <div class="icon contrast" on:click={() => close()} on:keypress={() => {}}>
<MdClose /> <MdClose />
</div> </div>
</div> </div>
@ -148,6 +148,7 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
@import '../../mixins';
.icon { .icon {
width: 25px; width: 25px;
height: 25px; height: 25px;
@ -183,7 +184,7 @@
span.name { span.name {
overflow: hidden; overflow: hidden;
word-wrap: break-word; word-wrap: break-word;
width: 100%;
} }
span:not(.name) { span:not(.name) {
position: relative; position: relative;
@ -191,6 +192,11 @@
font-size: 18px; font-size: 18px;
cursor: pointer; cursor: pointer;
} }
@include down(750px){
flex-direction: column;
gap: 10px;
}
} }
.examples { .examples {

View File

@ -1,43 +1,53 @@
<script lang="ts"> <script lang="ts">
import { form, field } from 'svelte-forms'; import { form, field } from "svelte-forms";
import { required, max, min } from 'svelte-forms/validators'; import { required, max, min } from "svelte-forms/validators";
import FileInput from '../forms/FileInput.svelte'; import FileInput from "../forms/FileInput.svelte";
import InputWithLabel from '../forms/InputWithLabel.svelte'; import InputWithLabel from "../forms/InputWithLabel.svelte";
import { getContext } from 'svelte'; import { getContext } from "svelte";
import { createExo, editExo } from '../../requests/exo.request'; import { createExo, editExo } from "../../requests/exo.request";
import type { Exercice } from '../../types/exo.type'; import type { Exercice } from "../../types/exo.type";
import { checkFile, errorMsg } from '../../utils/forms'; import { checkFile, errorMsg } from "../../utils/forms";
import { compareObject } from '../../utils/utils'; import { compareObject } from "../../utils/utils";
import { goto } from "$app/navigation";
export let editing = true; import ModalCard from "./ModalCard.svelte";
export let updateExo: Function = (e: Exercice) => {};
export let exo: Exercice | null = null; export let editing = true;
export let cancel: Function; export let updateExo: Function = (e: Exercice) => {
};
const { alert } = getContext<{ alert: Function }>('alert'); export let exo: Exercice | null = null;
export let cancel: Function;
// "Legally" initiate empty FileList for model field (simple list raises warning) const { alert } = getContext<{ alert: Function }>("alert");
/* let list = new DataTransfer(); const { show } = getContext<{ show: Function }>("modal");
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
list.items.add(file);
!editing && list.items.remove(0); */
// Initiate fields and form // "Legally" initiate empty FileList for model field (simple list raises warning)
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], { /* let list = new DataTransfer();
checkOnInit: true let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
}); list.items.add(file);
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true }); !editing && list.items.remove(0); */
const prv = field('private', !!exo ? exo.private : false);
const model = field('model', [], [checkFile(), required()], { // Initiate fields and form
checkOnInit: !editing const name = field("name", !!exo ? exo.name : "", [required(), max(50), min(5)], {
}); checkOnInit: true
const myForm = form(name, consigne, prv, model); });
const consigne = field("consigne", !!exo && exo.consigne != null ? exo.consigne : "", [max(200)], { checkOnInit: true });
const prv = field("private", !!exo ? exo.private : false);
const model = field("model", [], [checkFile(), required()], {
checkOnInit: !editing
});
const myForm = form(name, consigne, prv, model);
const exerciceStore = getContext<{ exerciceStore: any }>("exerciceStore");
const tagsStore = getContext<{ tagsStore: any }>("tagsStore");
const { navigate } = getContext<{ navigate: Function }>("navigation");
const { success, error } = getContext<{ success: Function, error: Function }>("notif");
let loading = false
</script> </script>
<form <form
action="" action=""
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
loading = true
if (editing && exo != null) { if (editing && exo != null) {
editExo(exo.id_code, { editExo(exo.id_code, {
name: $name.value, name: $name.value,
@ -45,54 +55,85 @@
private: $prv.value, private: $prv.value,
...($model.dirty == true && { file: $model.value[0] }) ...($model.dirty == true && { file: $model.value[0] })
}).then((r) => { }).then((r) => {
loading=false
success('Exercice modifié !', `Exercice ${r.data.name} modifié avec succès !`)
exo=r.data exo=r.data
updateExo(r.data); updateExo(r.data);
cancel() cancel()
}); }).catch((e) => {
loading=false
console.log(e)
error('Erreur', 'Une erreur est survenue lors de la modification de l\'exercice')
});
} else { } else {
createExo({ createExo({
name: $name.value, name: $name.value,
consigne: $consigne.value, consigne: $consigne.value,
private: $prv.value, private: $prv.value,
file: $model.value[0] file: $model.value[0]
}).then((r) => { }).then((r) => {
loading=false
updateExo(r.data); updateExo(r.data);
cancel() success('Exercice créé !', `Exercice ${r.data.name} créé avec succès !`)
}); goto(`/exercices/${r.data.id_code}`)
//cancel()
show(
ModalCard,
{
exo: r.data,
exos: exerciceStore,
tags: tagsStore
},
() => {
navigate(-1)
}, true
);
}).catch((e) => {
loading=false
error('Erreur', 'Une erreur est survenue lors de la création de l\'exercice')
});
} }
}} }}
> >
<InputWithLabel <InputWithLabel
type="text" type="text"
bind:value={$name.value} bind:value={$name.value}
maxlength="50" maxlength="50"
minlength="5" minlength="5"
required required
label="Nom" label="Nom"
errors={errorMsg($myForm, 'name')} errors={errorMsg($myForm, 'name')}
name="name" name="name"
/> />
<InputWithLabel <InputWithLabel
type="text" type="text"
bind:value={$consigne.value} bind:value={$consigne.value}
maxlength="200" maxlength="200"
label="Consigne" label="Consigne"
errors={errorMsg($myForm, 'consigne')} errors={errorMsg($myForm, 'consigne')}
name="consigne" name="consigne"
/> />
<div> <div>
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" /> <input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
<label for="private">Privé</label> <label for="private">Privé</label>
</div> </div>
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} defaultValue={editing &&exo!= null? exo.exo_source: null}/> <FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code}
defaultValue={editing &&exo!= null? exo.exo_source: null} />
<div class="wp-100"> <div class="wp-100">
<button class="primary-btn" disabled={!$myForm.valid}>Modifier</button> <button class="primary-btn" disabled={!$myForm.valid}>
<button {#if !loading}
class="danger-btn" {editing ? "Modifier" : "Créer"}
on:click|preventDefault={() => { {:else}
<span class="spinner"></span>
{/if}
</button>
<button
class="danger-btn"
on:click|preventDefault={() => {
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) { if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
alert({ alert({
@ -104,17 +145,23 @@
} else { } else {
cancel(); cancel();
} }
}}>Annuler</button }}>Annuler
> </button
</div> >
</div>
</form> </form>
<style> <style>
form { form {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
align-items: flex-start; align-items: flex-start;
} }
.spinner{
width: 15px;
height: 15px;
border-width: 2px!important;
}
</style> </style>

View File

@ -137,6 +137,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import '../../mixins';
.auth-head { .auth-head {
display: flex; display: flex;
@ -157,6 +158,11 @@
background-color: $background; background-color: $background;
padding: 20px; padding: 20px;
height: 100%; height: 100%;
min-width: 600px;
@include down(800){
min-width: 0;
width: 100%;
}
} }
.selected { .selected {

View File

@ -112,54 +112,59 @@
</script> </script>
{#if $tagStore.data != undefined}
<Head location={filter} bind:search bind:selected />
{/if}
{#if $tagStore.isFetching == true}
Fetching
{/if}
<div class="feed"> <div class="full" class:loading={$tagStore.isFetching || $exerciceStore.isFetching}>
<div class="title">
{#if filter == 'user'}
<h1> {#if $tagStore.data != undefined}
Vos <span>exercices</span> <Head location={filter} bind:search bind:selected />
</h1> {/if}
<p>
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
publics <div class="feed">
</p> <div class="title">
{#if filter == 'user'}
<h1>
Vos <span>exercices</span>
</h1>
<p>
Vous retrouverez ici tous les exercices que vous avez créé ou copié depuis les exercices
publics
</p>
{:else}
<h1>
Tous les <span>exercices</span>
</h1>
<p>Vous retrouverez ici tous les exercices créés par les autres utilisateurs</p>
{/if}
</div>
{#if $exerciceStore.data != undefined}
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
.map((s) => s.id_code)
.includes(t.id_code))) as e}
<Card bind:exo={e} />
{/each}
{#if $exerciceStore.data.items.length == 0}
<p class="empty">Aucun exercice</p>
{/if}
{:else} {:else}
<h1> {#each Array(10) as i}
Tous les <span>exercices</span> <div class="skeleton"><span /></div>
</h1> {/each}
<p>Vous retrouverez ici tous les exercices créés par les autres utilisateurs</p>
{/if} {/if}
</div> </div>
{#if $exerciceStore.data != undefined} {#if $exerciceStore.data != undefined}
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags <Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
.map((s) => s.id_code)
.includes(t.id_code))) as e}
<Card bind:exo={e} />
{/each}
{#if $exerciceStore.data.items.length == 0}
<p>Aucun exercices</p>
{/if}
{:else}
{#each Array(10) as i}
<div class="skeleton"><span /></div>
{/each}
{/if} {/if}
</div> </div>
{#if $exerciceStore.data != undefined}
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
{/if}
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
@import "../../mixins.scss";
.skeleton { .skeleton {
width: 330px; //width: 330px;
max-width: 330px;
height: 250px; height: 250px;
opacity: .8; opacity: .8;
@ -214,6 +219,10 @@
margin-top: 20px; margin-top: 20px;
} }
.full {
padding: 0 20px;
}
.title { .title {
grid-column: 1/3; grid-column: 1/3;
display: flex; display: flex;
@ -221,6 +230,11 @@
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@include down(600) {
grid-column: 1/2;
text-align: center;
}
h1 { h1 {
font-size: 3.5em; font-size: 3.5em;
font-weight: bolder; font-weight: bolder;
@ -235,4 +249,11 @@
color: $primary; color: $primary;
} }
} }
.empty{
display: flex;
justify-content: center;
align-items: center;
font-style: italic;
}
</style> </style>

View File

@ -70,6 +70,7 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
.head { .head {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -77,6 +78,17 @@
div { div {
width: 50%; width: 50%;
} }
@include down(600){
flex-direction: column;
gap: 10px;
> * {
width: 100%!important;
}
button{
width: 100%!important;
}
}
} }
.search { .search {
display: flex; display: flex;

View File

@ -51,6 +51,7 @@
}} }}
/> />
{:else if editing === true} {:else if editing === true}
<h1>Modification</h1>
<EditForm <EditForm
bind:exo bind:exo
cancel={() => { cancel={() => {
@ -63,8 +64,10 @@
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
@import '../../mixins';
.modal { .modal {
min-width: 820px; min-width: 800px;
background: $background; background: $background;
padding: 70px; padding: 70px;
grid-gap: 10px; grid-gap: 10px;
@ -73,5 +76,15 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 20px; gap: 20px;
@include down(800){
width: 100%;
height: 100%;
min-width: 0;
padding: 50px;
}
}
h1{
font-size: 1.8rem;
} }
</style> </style>

View File

@ -104,7 +104,6 @@
display: flex; display: flex;
margin: 30px; margin: 30px;
height: max-content; height: max-content;
width: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
button { button {

View File

@ -27,7 +27,7 @@
</script> </script>
<span class="inputLabel" class:error={errors.length !== 0}> <span class="inputLabel" class:error={errors.length !== 0}>
<div style:position="relative">
<input <input
use:typeAction use:typeAction
on:input={(e)=>{change(e)}} on:input={(e)=>{change(e)}}
@ -48,7 +48,7 @@
{/if} {/if}
</div> </div>
{/if} {/if}
</div>
<span class="bar" /> <span class="bar" />
{#if errors.length !== 0} {#if errors.length !== 0}

View File

@ -1,91 +1,91 @@
<script lang="ts"> <script lang="ts">
import type { import type {
Challenge, Challenge,
Member, Member,
ParcoursInfos, ParcoursInfos,
Room, Room,
Note as NoteType Note as NoteType
} from '../../types/room.type'; } from "../../types/room.type";
import { getContext, onDestroy, onMount } from "svelte"; import { getContext, onDestroy, onMount } from "svelte";
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from "svelte/store";
import { challenge, corrigeChallenge, getChallenge, getParcours, sendChallenge } from '../../requests/room.request'; import { challenge, corrigeChallenge, getChallenge, getParcours, sendChallenge } from "../../requests/room.request";
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import { page } from '$app/stores'; import { page } from "$app/stores";
import InputChallenge from './InputChallenge.svelte'; import InputChallenge from "./InputChallenge.svelte";
import { parseTimer } from '../../utils/utils'; import { parseTimer } from "../../utils/utils";
import FaUndo from 'svelte-icons/fa/FaUndo.svelte'; import FaUndo from "svelte-icons/fa/FaUndo.svelte";
const room: Writable<Room> = getContext('room'); const room: Writable<Room> = getContext("room");
const member: Writable<Member> = getContext('member'); const member: Writable<Member> = getContext("member");
const challengeStore: Writable<{ const challengeStore: Writable<{
challenge: Challenge[]; challenge: Challenge[];
id_code: string; id_code: string;
parcours: ParcoursInfos; parcours: ParcoursInfos;
corriged: boolean; corriged: boolean;
mistakes?: number mistakes?: number
validated?: boolean; validated?: boolean;
challenger?: { name: string }; challenger?: { name: string };
isCorriged?: boolean, isCorriged?: boolean,
} | null> = writable(null); } | null> = writable(null);
export let id_code: string; export let id_code: string;
export let corrige: boolean = false; export let corrige: boolean = false;
onMount(()=>{ onMount(() => {
if(!corrige) { if (!corrige) {
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => { challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, corriged: false }); challengeStore.set({ ...p, corriged: false });
}); });
} else { } else {
getChallenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => { getChallenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, challenge: p.data, note: {...p.note, temporary: !p.isCorriged}, corriged: true }); challengeStore.set({ ...p, challenge: p.data, note: { ...p.note, temporary: !p.isCorriged }, corriged: true });
remaining = p.time; remaining = p.time;
}); });
} }
}) });
let timer: number | null = null;
let remaining: number | null = null;
const {error, success} = getContext("notif");
$: {
if (!corrige && $challengeStore != null && remaining == null) {
remaining = $challengeStore.parcours.time * 60;
}
}
let timer: number | null = null; $: {
let remaining: number | null = null; if (!corrige && $challengeStore != null && timer == null && remaining != null) {
timer = window.setInterval(() => {
remaining = remaining! - 1;
}, 1000);
}
}
$: { onDestroy(() => {
if (!corrige && $challengeStore != null && remaining == null) { if (timer != null) {
remaining = $challengeStore.parcours.time * 60; clearInterval(timer);
} }
} });
$: {
if (!corrige && $challengeStore != null && timer == null && remaining != null) {
timer = window.setInterval(() => {
remaining = remaining! - 1;
}, 1000);
}
}
onDestroy(() => {
if (timer != null) {
clearInterval(timer);
}
});
</script> </script>
{#if $challengeStore != null} <div class="full">
<div class="head"> {#if $challengeStore != null}
<h1> <div class="head">
{$challengeStore.parcours.name} <h1>
{$challengeStore.parcours.name}
{#if corrige && !!$challengeStore.challenger && remaining != null} {#if corrige && !!$challengeStore.challenger && remaining != null}
<span class="correction-info"> <span class="correction-info">
- Correction de <span class="italic">{$challengeStore.parcours.name}</span> par Correction de <span class="italic">{$challengeStore.parcours.name}</span> par
<span class="italic underline">{$challengeStore.challenger.name}</span> <span class="italic underline">{$challengeStore.challenger.name}</span>
en {parseTimer(remaining)} (cliquez sur les réponses pour voir)</span en {parseTimer(remaining)} (cliquez sur les réponses pour voir)</span
> >
{:else} {:else}
<span <span
class="icon" class="icon"
on:click={() => { on:click={() => {
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then( challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then(
(p) => { (p) => {
challengeStore.set({ ...p, corriged: false }); challengeStore.set({ ...p, corriged: false });
@ -97,63 +97,67 @@
} }
); );
}} }}
title={'Réessayer'} title={'Réessayer'}
on:keydown={() => {}}><FaUndo /></span on:keydown={() => {}}><FaUndo /></span
> >
{/if} {/if}
</h1> </h1>
{#if $challengeStore.mistakes}
{$challengeStore.mistakes} fautes
{/if}
{#if !corrige}
<p
class="timer"
class:oneminute={remaining != null && remaining < 60}
class:late={(remaining != null && remaining < 0) ||
[9, 7, 5, 3, 1].includes(remaining != null ? remaining : 0)}
>
{remaining != null && parseTimer(remaining)}
</p>
{/if}
</div>
{#each $challengeStore.challenge as e, d (`${$challengeStore.id_code}_${d}`)} {#if $challengeStore.mistakes}
<div class="exo"> <p class="mistakes" class:validated={$challengeStore.validated}>{$challengeStore.mistakes} fautes</p>
<div class="infos"> {/if}
<h2>Exercice {d + 1} : <span>{e.exo.name}</span></h2>
<p> {#if !corrige}
{e.exo.consigne} <p
</p> class="timer"
</div> class:oneminute={remaining != null && remaining < 60}
<div class="data"> class:late={(remaining != null && remaining < 0) ||
{#each e.data as c, a} [9, 7, 5, 3, 1].includes(remaining != null ? remaining : 0)}
<div class="calcul"> >
{#each c.calcul.replace(']', '] ').replace('[', ' [').split(' ') as i, b} {remaining != null && parseTimer(remaining)}
{#if i.startsWith('[') && i.endsWith(']')} </p>
<InputChallenge {/if}
bind:value={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].value} </div>
bind:correction={c.inputs[parseInt(i.replace('[', '').replace(']', ''))]
{#each $challengeStore.challenge as e, d (`${$challengeStore.id_code}_${d}`)}
<div class="exo">
<div class="infos">
<h2>Exercice {d + 1} : <span>{e.exo.name}</span></h2>
{#if e.exo.consigne != null}
<p>
{e.exo.consigne}
</p>
{/if}
</div>
<div class="data">
{#each e.data as c, a}
<div class="calcul">
{#each c.calcul.replace(']', '] ').replace('[', ' [').split(' ') as i, b}
{#if i.startsWith('[') && i.endsWith(']')}
<InputChallenge
bind:value={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].value}
bind:correction={c.inputs[parseInt(i.replace('[', '').replace(']', ''))]
.correction} .correction}
corriged={$challengeStore.corriged} corriged={$challengeStore.corriged}
bind:valid={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].valid} bind:valid={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].valid}
corrigeable={corrige} corrigeable={corrige}
admin = {$member.isAdmin} admin={$member.isAdmin}
/> />
{:else} {:else}
{i}{' '} {i}{' '}
{/if} {/if}
{/each} {/each}
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
{/each} {/each}
<div> <div>
{#if !corrige} {#if !corrige}
<button <button
hidden={$challengeStore.corriged} hidden={$challengeStore.corriged}
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
if ($challengeStore == null || remaining == null) return; if ($challengeStore == null || remaining == null) return;
sendChallenge( sendChallenge(
$room.id_code, $room.id_code,
@ -175,12 +179,13 @@
clearInterval(timer); clearInterval(timer);
} }
}); });
}}>Valider !</button }}>Valider !
> </button
<button >
hidden={!$challengeStore.corriged} <button
class="primary-btn" hidden={!$challengeStore.corriged}
on:click={() => { class="primary-btn"
on:click={() => {
console.log('RETRY CLICKED') console.log('RETRY CLICKED')
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => { challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
challengeStore.set({ ...p, corriged: false }); challengeStore.set({ ...p, corriged: false });
@ -190,107 +195,149 @@
timer = null; timer = null;
} }
}); });
}}>Réessayer !</button }}>Réessayer !
> </button
{:else if $member.isAdmin} >
<button {:else if $member.isAdmin}
hidden={!$challengeStore.corriged} <button
class="primary-btn" hidden={!$challengeStore.corriged}
on:click={() => { class="primary-btn"
on:click={() => {
corrigeChallenge($room.id_code, id_code,$challengeStore?.challenge, !$member.isUser ? $member.clientId : null).then((p) => { corrigeChallenge($room.id_code, id_code,$challengeStore?.challenge, !$member.isUser ? $member.clientId : null).then((p) => {
if($challengeStore == null) return if($challengeStore == null) return
$challengeStore.challenge = p.data $challengeStore.challenge = p.data
$challengeStore.mistakes = p.mistakes $challengeStore.mistakes = p.mistakes
$challengeStore.validated = p.validated $challengeStore.validated = p.validated
success('Corrigé !', 'Le challenge a été corrigé avec succès !')
}).catch(()=>{
error('Erreur', 'Une erreur est survenue lors de la correction du challenge')
}); });
}}>Valider !</button }}>Valider !
> </button
{/if} >
{/if}
<button <button
class="danger-btn" class="danger-btn"
on:click={() => { on:click={() => {
if ($challengeStore == null) return; if ($challengeStore == null) return;
goto(`?${new URLSearchParams({p: $challengeStore.parcours.id_code}).toString()}`); goto(`?${new URLSearchParams({p: $challengeStore.parcours.id_code}).toString()}`);
}}>{!$challengeStore.corriged?"Annuler !":"Retour"}</button }}>{!$challengeStore.corriged ? "Annuler !" : "Retour"}</button
> >
</div> </div>
{/if} {/if}
</div>
<style lang="scss"> <style lang="scss">
.timer { @import "../../mixins";
font-size: 2em;
color: $green;
font-weight: 800;
}
.oneminute { .full {
color: $orange; padding: 7px 15px;
} }
.head { .timer {
display: flex; font-size: 2em;
align-items: center; color: $green;
justify-content: space-between; font-weight: 800;
margin: 40px 0; }
min-height: 70px;
}
.late { .oneminute {
color: $red; color: $orange;
} }
.calcul { .head {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: space-between;
position: relative; margin: 40px 0;
} min-height: 70px;
.data { @include down(800px) {
display: grid; flex-direction: column;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); text-align: center;
gap: 10px; h1{
} flex-direction: column;
}
}
.infos { @include up(800px) {
h2 { .correction-info::before{
font-size: 1.2em; content: " - ";
span { }
font-style: italic; }
}
}
p {
font-size: 1em;
font-style: italic;
text-decoration: underline;
}
margin-bottom: 10px;
}
.exo { }
margin-bottom: 30px;
margin-top: 20px; .late {
} color: $red;
.icon { }
height: 20px;
width: 20px; .calcul {
cursor: pointer; display: flex;
display: flex; align-items: center;
transition: 0.2s; gap: 10px;
&:hover { position: relative;
transform: rotate(360deg); }
}
} .data {
h1 { display: grid;
display: flex; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
align-items: center; gap: 10px;
gap: 20px; }
font-size: 2.3em;
} .infos {
.correction-info { h2 {
font-size: 0.6em; font-size: 1.2em;
color: grey;
font-weight: 600; span {
} font-style: italic;
}
}
p {
font-size: 1em;
font-style: italic;
text-decoration: underline;
}
margin-bottom: 10px;
}
.exo {
margin-bottom: 30px;
margin-top: 20px;
}
.icon {
height: 20px;
width: 20px;
cursor: pointer;
display: flex;
transition: 0.2s;
&:hover {
transform: rotate(360deg);
}
}
h1 {
display: flex;
align-items: center;
gap: 20px;
font-size: 2.3em;
}
.correction-info {
font-size: 0.6em;
color: grey;
font-weight: 600;
}
.mistakes {
font-weight: 700;
font-size: 2em;
color: $red;
}
.validated {
color: $green;
}
</style> </style>

View File

@ -1,113 +1,134 @@
<script lang="ts"> <script lang="ts">
import type { Member, ParcoursRead } from '../../types/room.type'; import type { Member, ParcoursRead } from "../../types/room.type";
import { getContext } from 'svelte'; import { getContext } from "svelte";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
import IoIosArrowDown from 'svelte-icons/io/IoIosArrowDown.svelte'; import IoIosArrowDown from "svelte-icons/io/IoIosArrowDown.svelte";
import { goto } from '$app/navigation'; import { goto } from "$app/navigation";
import { parseTimer } from '../../utils/utils'; import { parseTimer } from "../../utils/utils";
import IoMdOpen from 'svelte-icons/io/IoMdOpen.svelte'; import IoMdOpen from "svelte-icons/io/IoMdOpen.svelte";
const parcours: Writable<ParcoursRead | null> = getContext('parcours'); const parcours: Writable<ParcoursRead | null> = getContext("parcours");
const member: Writable<Member | null> = getContext('member'); const member: Writable<Member | null> = getContext("member");
let selected = "";
let selected = '';
</script> </script>
{#if $parcours != null && $member != null} {#if $parcours != null && $member != null}
<div class="trylist"> <div class="trylist">
{#if Object.keys($parcours.challenges).length == 0} {#if Object.keys($parcours.challenges).length == 0}
<p class="italic">Aucun essai effectué :(</p> <p class="italic">Aucun essai effectué :(</p>
{/if} {/if}
{#each Object.entries($parcours.challenges) as [id, chall]} {#each Object.entries($parcours.challenges) as [id, chall]}
<p <p
on:click={() => { on:click={() => {
selected = selected == chall.challenger.id_code ? '' : chall.challenger.id_code; selected = selected == chall.challenger.id_code ? '' : chall.challenger.id_code;
}} }}
class:selected={selected == chall.challenger.id_code} class:selected={selected == chall.challenger.id_code}
class="tries" class="tries"
on:keydown={() => {}} on:keydown={() => {}}
> >
<span class="icon"><IoIosArrowDown /></span> <span class="icon"><IoIosArrowDown /></span>
{chall.challenger.id_code == $member.id_code ? 'Vos essais' : chall.challenger.name} {chall.challenger.id_code == $member.id_code ? 'Vos essais' : chall.challenger.name}
</p> <span class:valid = {chall.challenger.validated} class="validation-status">{chall.challenger.validated ? "Validé": "Non validé"}</span>
</p>
{#if selected == chall.challenger.id_code} {#if selected == chall.challenger.id_code}
{#each chall.challenges as c} {#each chall.challenges as c}
<div <div
class="try" class="try"
on:click={() => { on:click={() => {
goto(`?${new URLSearchParams({corr: c.id_code}).toString()}`); goto(`?${new URLSearchParams({corr: c.id_code}).toString()}`);
}} }}
on:keydown={() => {}} on:keydown={() => {}}
title="Voir la correction" title="Voir la correction"
> >
<p><span class:validated={c.validated} class:uncorriged={!c.isCorriged} class="note" <p><span class:validated={c.validated} class:uncorriged={!c.isCorriged} class="note"
>{c.mistakes} faute{c.mistakes > 1 ?"s": ""} </span> en <strong >{parseTimer(c.time)}</strong> >{c.mistakes} faute{c.mistakes > 1 ? "s" : ""} </span> en <strong>{c.time < $parcours.time * 60 ? parseTimer(c.time): parseTimer($parcours.time*60)} <span title={`Vous avez dépassé de ${parseTimer(c.time - $parcours.time*60)}`} class="time-overflow">{c.time > $parcours.time * 60? `( + ${parseTimer(c.time - $parcours.time*60)} )`:""}</span></strong>
</p> </p>
<span class="corrige-link icon"><IoMdOpen /></span> <span class="corrige-link icon"><IoMdOpen /></span>
</div> </div>
{/each} {/each}
{/if} {/if}
{/each} {/each}
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.tries { .tries {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
gap: 5px; gap: 5px;
align-items: center; align-items: center;
transition: 0.3s; transition: 0.3s;
.icon {
width: 20px;
display: flex;
align-items: center;
transform: rotate(-90deg);
transition: 0.2s;
}
}
.selected {
font-weight: 700;
.icon {
transform: rotate(0);
}
}
.try { .icon {
display: flex; width: 20px;
gap: 10px; display: flex;
cursor: pointer; align-items: center;
width: max-content; transform: rotate(-90deg);
margin-left: 30px; transition: 0.2s;
p span{ }
color: $red; }
font-weight: 600;
}
}
.icon { .selected {
display: flex; font-weight: 700;
align-items: center;
width: 20px; .icon {
} transform: rotate(0);
}
}
.try {
display: flex;
gap: 10px;
cursor: pointer;
width: max-content;
margin-left: 30px;
p > span {
color: $red;
font-weight: 600;
}
}
.icon {
display: flex;
align-items: center;
width: 20px;
}
.corrige-link {
color: $primary;
}
.uncorriged {
color: grey;
font-weight: 900;
}
.trylist {
display: flex;
flex-direction: column;
gap: 10px;
}
.validation-status{
color: $red;
font-weight: 600;
&::before{
content: " - ";
}
}
.validated, .valid {
color: $green !important;
}
.time-overflow{
color: $red;
font-weight: 400;
font-size: .8em;
}
.corrige-link {
color: $primary;
}
.uncorriged {
color: grey;
font-weight: 900;
}
.trylist {
display: flex;
flex-direction: column;
gap: 10px;
}
.validated {
color: $green !important;
}
</style> </style>

View File

@ -4,11 +4,15 @@
import FaLock from "svelte-icons/fa/FaLock.svelte"; import FaLock from "svelte-icons/fa/FaLock.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import FaTimes from "svelte-icons/fa/FaTimes.svelte";
import FaCheck from "svelte-icons/fa/FaCheck.svelte";
import MdCheck from "svelte-icons/md/MdCheck.svelte";
import MdClose from "svelte-icons/md/MdClose.svelte";
const room: Writable<Room> = getContext("room"); const room: Writable<Room> = getContext("room");
const member: Writable<Member> = getContext("member"); const member: Writable<Member> = getContext("member");
const { send } = getContext<{ send: Function }>("ws"); const { send } = getContext<{ send: Function }>("ws");
const { alert } = getContext<{ alert: Function }>("alert");
$: online = $: online =
$room != null $room != null
? $room.members.filter((r): r is Member => "online" in r && r.online == true) ? $room.members.filter((r): r is Member => "online" in r && r.online == true)
@ -21,6 +25,16 @@
$room != null $room != null
? $room.members.filter((r): r is Waiter => "waiter_id" in r && !!r.waiter_id) ? $room.members.filter((r): r is Waiter => "waiter_id" in r && !!r.waiter_id)
: []; : [];
const ban = (m: Member) => {
if (!$member.isAdmin || m.isAdmin) return;
alert(
{
title: "Bannir", description: "Êtes-vous sûr de vouloir bannir cet utilisateur ?", validate: () => {
send("ban", { member_id: m.id_code });
}, validateButton: "Bannir"
}
);
};
</script> </script>
<div class="members"> <div class="members">
@ -62,9 +76,7 @@
class:member={m.id_code == $member.id_code} class:member={m.id_code == $member.id_code}
class="online" class="online"
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''} title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
on:click={() => { on:click={(e)=>ban(m)}
$member.isAdmin && !m.isAdmin && send('ban', { member_id: m.id_code });
}}
on:keydown={() => {}} on:keydown={() => {}}
> >
{m.username} {m.username}
@ -86,9 +98,7 @@
class:bannable={m.id_code != $member.id_code && $member.isAdmin && !m.isAdmin} class:bannable={m.id_code != $member.id_code && $member.isAdmin && !m.isAdmin}
class:member={m.id_code == $member.id_code} class:member={m.id_code == $member.id_code}
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''} title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
on:click={() => { on:click={(e)=>ban(m)}
$member.isAdmin && !m.isAdmin && send('ban', { member_id: m.id_code });
}}
on:keydown={() => {}} on:keydown={() => {}}
> >
{m.username} {m.username}
@ -111,14 +121,20 @@
class="accept" class="accept"
on:click={() => { on:click={() => {
send('accept', { waiter_id: m.waiter_id }); send('accept', { waiter_id: m.waiter_id });
}}>Accept }}
title="Accepter"
>
<MdCheck />
</button </button
> >
<button <button
class="refuse" class="refuse"
on:click={() => { on:click={() => {
send('refuse', { waiter_id: m.waiter_id }); send('refuse', { waiter_id: m.waiter_id });
}}>Refuse }}
title="Refuser"
>
<MdClose />
</button </button
> >
</p> </p>
@ -171,6 +187,9 @@
font-size: 1em; font-size: 1em;
width: max-content; width: max-content;
display: flex;
align-items: center;
span { span {
color: grey; color: grey;
font-size: 0.9em; font-size: 0.9em;
@ -203,4 +222,27 @@
font-weight: 500; font-weight: 500;
font-size: 0.8em !important; font-size: 0.8em !important;
} }
.accept, .refuse {
border: none;
background-color: transparent;
cursor: pointer;
width: 30px;
height: 30px;
transition: .3s;
margin: 0;
&:hover {
transform: scale(1.1);
}
}
.accept {
margin-left: 5px;
color: $green;
}
.refuse {
color: $red;
}
</style> </style>

View File

@ -13,6 +13,8 @@
import { messages, handlers } from "../../store/ws"; import { messages, handlers } from "../../store/ws";
import Stats from "./Stats.svelte"; import Stats from "./Stats.svelte";
import ChallengesList from "./ChallengesList.svelte"; import ChallengesList from "./ChallengesList.svelte";
import { delParcours } from "../../requests/room.request.js";
import FaRegTrashAlt from "svelte-icons/fa/FaRegTrashAlt.svelte";
export let id_code: string; export let id_code: string;
@ -20,7 +22,7 @@
const member: Writable<Member> = getContext("member"); const member: Writable<Member> = getContext("member");
const { send } = getContext<{ send: Function }>("ws"); const { send } = getContext<{ send: Function }>("ws");
const {alert} = getContext<{alert: Function}>("alert");
const parcours: Writable<ParcoursRead | null> = getContext("parcours"); const parcours: Writable<ParcoursRead | null> = getContext("parcours");
let open = ""; let open = "";
@ -53,6 +55,27 @@
> >
<FaEdit /> <FaEdit />
</div> </div>
<div
class="icon back"
on:keydown={() => {}}
on:click|stopPropagation={() => {
alert({
title: 'Supprimer ?',
description: 'Voulez vous supprimer ce parcours ?',
validate: () => {
delParcours(
$room?.id_code,
$parcours.id_code,
!$member.isUser ? $member?.clientId : null
).then(()=>{
goto(`?${new URLSearchParams().toString()}`);
});
}
});
}}
>
<FaRegTrashAlt />
</div>
{/if} {/if}
<div <div
@ -187,6 +210,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
width: 20px; width: 20px;
height: 20px;
} }
@ -235,6 +259,7 @@
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
align-items: stretch; align-items: stretch;
padding: 3px 15px;
} }
.btn { .btn {
@ -269,4 +294,7 @@
.edit { .edit {
color: $green; color: $green;
} }
h1{
word-break: break-word;
}
</style> </style>

View File

@ -1,128 +1,150 @@
<script lang="ts"> <script lang="ts">
import { delParcours } from '../../requests/room.request'; import { getContext } from "svelte";
import { getContext } from 'svelte'; import type { Writable } from "svelte/store";
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte'; import type { Member, Room } from "../../types/room.type";
import type { Writable } from 'svelte/store'; import { goto } from "$app/navigation";
import type { Member, Room } from '../../types/room.type';
import { goto } from '$app/navigation'; const room: Writable<Room> = getContext("room");
import { page } from '$app/stores'; const member: Writable<Member> = getContext("member");
const room: Writable<Room> = getContext('room');
const member: Writable<Member> = getContext('member');
const { alert } = getContext<{ alert: Function }>('alert');
</script> </script>
<div class="parcours"> <div class="parcours">
<div class="head"> <div class="head">
<h2>Parcours</h2> <h2>Parcours</h2>
{#if $member.isAdmin} {#if $member.isAdmin}
<button <button
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
goto(`?${new URLSearchParams({ p: 'new' }).toString()}`); goto(`?${new URLSearchParams({ p: 'new' }).toString()}`);
}}>Nouveau</button }}>Nouveau
> </button
{/if} >
</div> {/if}
</div>
<div class="list"> <div class="list">
{#if $room.parcours.length == 0} {#if $room.parcours.length == 0}
<p class="empty">Aucun parcours pour le moment</p> <p class="empty">Aucun parcours pour le moment</p>
{/if} {/if}
{#each $room.parcours as p} {#each $room.parcours as p}
<div <div
on:click={() => { on:click={() => {
goto(`?${new URLSearchParams({ p: p.id_code }).toString()}`); goto(`?${new URLSearchParams({ p: p.id_code }).toString()}`);
}} }}
on:keydown={() => {}} on:keydown={() => {}}
> >
<p>{p.name}</p> <p class="parcours-name">{p.name}</p>
{#if p.best_note}
<p>Record : {p.best_note} fautes</p>
{:else}
Aucun essai effectué
{/if}
{#if p.validated}
<p data-testid="valid">Parcours validé</p>
{/if}
{#if $member.isAdmin}
<div <div class="stats">
class="icon delete" <p class="stat">
on:keydown={() => {}} {#if p.best_note}
on:click|stopPropagation={() => { <strong>Record :</strong> {p.best_note} faute{p.best_note > 1 ? "s" : ""}
alert({ {:else}
title: 'Supprimer ?', Aucun essai effectué
description: 'Voulez vous supprimer ce parcours ?', {/if}
validate: () => { </p>
delParcours( <p class="stat valid" data-testid="valid"
$room?.id_code, class:validated={p.validated}>{ p.validated ? "Parcours validé" : "Parcours non validé"}</p>
p.id_code,
!$member.isUser ? $member?.clientId : null </div>
);
}
}); </div>
}} {/each}
> </div>
<FaRegTrashAlt />
</div>
{/if}
</div>
{/each}
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
.empty { .empty {
text-align: center; text-align: center;
font-style: italic; font-style: italic;
margin: 30px 0; margin: 30px 0;
} }
.parcours {
background-color: rgba($background, 0.4);
border: 1px solid $border;
padding: 20px; .parcours {
width: 100%; background-color: rgba($background, 0.4);
height: 100%; border: 1px solid $border;
display: flex; padding: 20px;
flex-direction: column; width: 100%;
border-radius: 5px; height: 100%;
} display: flex;
.head { flex-direction: column;
display: flex; border-radius: 5px;
align-items: center; }
justify-content: space-between;
border-bottom: 1px solid $border; .head {
padding: 10px 0; display: flex;
button { align-items: center;
width: max-content; justify-content: space-between;
} border-bottom: 1px solid $border;
} padding: 10px 0;
.icon {
width: 18px; button {
height: 18px; width: max-content;
color: $red; }
transition: 0.4s; }
opacity: 0;
&:hover { .icon {
transform: scale(1.1); width: 18px;
} height: 18px;
} color: $red;
.list { transition: 0.4s;
overflow: auto; opacity: 0;
> div {
padding: 30px 10px; &:hover {
border-bottom: 1px solid $border-light; transform: scale(1.1);
cursor: pointer; }
transition: 0.3s; }
display: flex;
align-items: center; .list {
justify-content: space-between; overflow: auto;
&:hover {
background-color: lighten($color: $background, $amount: 10); > div {
.icon { padding: 30px 10px;
opacity: 1; border-bottom: 1px solid $border-light;
} cursor: pointer;
} transition: 0.3s;
} display: flex;
} align-items: center;
justify-content: space-between;
&:hover {
background-color: lighten($color: $background, $amount: 10);
.icon {
opacity: 1;
}
}
}
}
.valid {
font-weight: 600;
color: $red;
}
.validated {
color: $green;
}
.stat{
text-align: justify;
height: 1em;
margin: 5px 0;
width: max-content;
&::after{
content: "";
display: inline-block;
width: 100%;
}
}
.parcours-name {
font-weight: 500;
margin: 0;
word-break: break-word;
}
</style> </style>

View File

@ -1,47 +1,50 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from "svelte";
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte'; import FaRegTrashAlt from "svelte-icons/fa/FaRegTrashAlt.svelte";
import FaUndo from 'svelte-icons/fa/FaUndo.svelte'; import FaUndo from "svelte-icons/fa/FaUndo.svelte";
import FaTimes from 'svelte-icons/fa/FaTimes.svelte'; import FaTimes from "svelte-icons/fa/FaTimes.svelte";
import { goto } from '$app/navigation'; import IoMdLogOut from 'svelte-icons/io/IoMdLogOut.svelte'
import type { Writable } from 'svelte/store'; import { goto } from "$app/navigation";
import type { Room, Member } from '../../types/room.type'; import type { Writable } from "svelte/store";
import type ReconnectingWebSocket from 'reconnecting-websocket'; import type { Member, Room } from "../../types/room.type";
const room: Writable<Room> = getContext('room'); import type ReconnectingWebSocket from "reconnecting-websocket";
const member: Writable<Member>= getContext('member'); import { deleteRoom } from "../../requests/room.request.js";
const { send, ws } = getContext<{send: Function, ws: ReconnectingWebSocket}>('ws');
let editing = false; const room: Writable<Room> = getContext("room");
let name = $room.name; const member: Writable<Member> = getContext("member");
let r: HTMLInputElement; const { send, ws } = getContext<{ send: Function, ws: ReconnectingWebSocket }>("ws");
const { alert } = getContext<{ alert: Function }>("alert");
let editing = false;
let name = $room.name;
let r: HTMLInputElement;
</script> </script>
<div class="head"> <div class="head">
<div class="title"> <div class="title">
{#if !editing} {#if !editing}
<h1 <h1
on:dblclick={() => { on:dblclick={() => {
if(!$member.isAdmin) return if(!$member.isAdmin) return
editing = true; editing = true;
name = $room.name; name = $room.name;
r.focus(); r.focus();
console.log("OPENED") console.log("OPENED")
}} }}
> >
{$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span> {$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span>
</h1> </h1>
{/if} {/if}
<input <input
type="text" bind:this={r}
class="input" bind:value={name}
class:hide={!editing} class="input"
on:focusout={() => { class:hide={!editing}
on:focusout={() => {
editing = false; editing = false;
}} }}
bind:value={name} on:keydown={(e) => {
bind:this={r}
on:keydown={(e) => {
if (e.key == 'Escape') { if (e.key == 'Escape') {
editing = false; editing = false;
} else if (e.key == 'Enter') { } else if (e.key == 'Enter') {
@ -49,102 +52,134 @@
editing = false; editing = false;
} }
}} }}
/> type="text"
</div> />
</div>
<div class="icons"> <div class="icons">
{#if $member.isAdmin} {#if $member.isAdmin}
<div class="icon trash" data-testid="delete"><FaRegTrashAlt /></div> <div class="icon trash" data-testid="delete" on:click={()=>{
{/if} alert({title:"Voulez vous vraiment supprimer la salle ?",
description:"L'intégralité des données de la salle sera supprimée et vous ne pourrez plus y accéder",
<div validate: ()=>{
class="icon refresh" deleteRoom($room.id_code, !$member.isUser ? $member.clientId: null)
on:click={() => { },
console.log(ws) validateButton: "Supprimer la salle"})
//goto('/room/join')
}}>
<FaRegTrashAlt />
</div>
{:else}
<div class="icon trash" data-testid="leave" on:click={()=>{
alert({title:"Voulez vous vraiment quitter la salle ?",
description:"Vous perdrez l'intégralité de vos résultats et ne pourrez plus accéder à cette salle sans y avoir été accepté à nouveau",
validate: ()=>{
send('leave')
},
validateButton: "Quitter la salle"})
//goto('/room/join')
}}
on:keypress={()=>{}}
>
<IoMdLogOut />
</div>
{/if}
<div
class="icon refresh"
data-testid="refresh"
on:click={() => {
ws.reconnect(); ws.reconnect();
}} }}
on:keydown={() => {}} on:keydown={() => {}}
data-testid="refresh" >
> <FaUndo />
<FaUndo /> </div>
</div> <div
<div class="icon trash"
data-testid="leave" data-testid="leave"
class="icon trash" on:click={() => {
on:click={() => {
ws.close(); ws.close();
goto('/room/join') goto('/room/join')
}} }}
on:keydown={() => {}} on:keydown={() => {}}
> >
<FaTimes /> <FaTimes />
</div> </div>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.hide { .hide {
width: 0; width: 0;
height: 0; height: 0;
padding: 0; padding: 0;
border: none; border: none;
opacity: 0; opacity: 0;
} }
.title {
max-width: 50%;
margin: 10px 0;
font-size: 2em;
padding: 0 10px;
// height: 50px;
input {
font-size: inherit;
font-weight: 700;
transition: all 0s;
transition: border 0.3s;
padding-left: 0;
padding-top: 0;
}
h1 {
margin: 0;
font-weight: 700;
font-size: inherit;
span {
font-size: 0.5em;
color: grey;
}
}
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.icon { .title {
width: 20px; max-width: 50%;
height: 20px; margin: 10px 0;
transition: 0.2s; font-size: 2em;
cursor: pointer; padding: 0 10px;
display: flex; // height: 50px;
&:hover { input {
transform: scale(1.1); font-size: inherit;
} font-weight: 700;
} transition: all 0s;
transition: border 0.3s;
padding-left: 0;
padding-top: 0;
}
.trash { h1 {
color: $red; margin: 0;
} font-weight: 700;
font-size: inherit;
.refresh { span {
color: $contrast; font-size: 0.5em;
&:hover { color: grey;
transform: rotate(-360deg); }
} }
} }
.icons { .head {
display: flex; display: flex;
gap: 20px; align-items: center;
align-items: center; justify-content: space-between;
} }
.icon {
width: 20px;
height: 20px;
transition: 0.2s;
cursor: pointer;
display: flex;
&:hover {
transform: scale(1.1);
}
}
.trash {
color: $red;
}
.refresh {
color: $contrast;
&:hover {
transform: rotate(-360deg);
}
}
.icons {
display: flex;
gap: 20px;
align-items: center;
}
</style> </style>

View File

@ -1,118 +1,137 @@
<script lang="ts"> <script lang="ts">
import type { ParcoursRead, Member } from '../../types/room.type'; import type { ParcoursRead, Member } from "../../types/room.type";
import { parseTimer, statsCalculator } from '../../utils/utils'; import { parseTimer, statsCalculator } from "../../utils/utils";
import { getContext } from 'svelte'; import { getContext } from "svelte";
import type { Writable } from 'svelte/store'; import type { Writable } from "svelte/store";
import Classement from './Classement.svelte'; import Classement from "./Classement.svelte";
const parcours: Writable<ParcoursRead | null> = getContext('parcours'); const parcours: Writable<ParcoursRead | null> = getContext("parcours");
const member: Writable<Member | null> = getContext('member'); const member: Writable<Member | null> = getContext("member");
$: stats = $: stats =
$parcours != null && $member != null && !!$parcours.challenges[$member.id_code] $parcours != null && $member != null && !!$parcours.challenges[$member.id_code]
? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes)) ? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes))
: null; : null;
</script> </script>
{#if $parcours != null} {#if $parcours != null}
<div class="stats"> <div class="stats">
<h1 class:validated={$parcours.validated}> <h1 class:validated={$parcours.validated}>
{$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'} {$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'}
</h1> </h1>
<div class="statistics"> <div class="statistics">
<p data-testid="avg"> <p data-testid="avg">
<strong>Moyenne</strong> <strong>Moyenne</strong>
{stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined {stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined
? `${stats?.avg.toFixed(2)} fautes` ? `${stats?.avg.toFixed(2)} fautes`
: '-'} : '-'}
</p> </p>
<p data-testid="max"> <p data-testid="max">
<strong>Pire</strong> <strong>Pire</strong>
{stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'} {stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'}
</p> </p>
<p data-testid="min"> <p data-testid="min">
<strong>Meilleur</strong> <strong>Meilleur</strong>
{stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'} {stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'}
</p> </p>
</div> </div>
<div class="classement"> <div class="classement">
<h2>Classements</h2> <h2>Classements</h2>
<div class="ranks"> <div class="ranks">
<div> <div>
<h3>Top élèves</h3> <h3>Top élèves</h3>
<Classement <Classement
tops={$parcours.ranking.map((t) => { tops={$parcours.ranking.map((t) => {
return { name: t.name, value: `Moyenne : ${t.avg.toFixed(2)} fautes` }; return { name: t.name, value: `Moyenne : ${t.avg.toFixed(2)} fautes` };
})} })}
rank={$parcours.memberRank != null && $parcours.memberRank > 3 && $member != null rank={$parcours.memberRank != null && $parcours.memberRank > 3 && $member != null
? { ? {
name: $member.username, name: $member.username,
rank: $parcours.memberRank, rank: $parcours.memberRank,
value: `Moyenne : ${stats?.avg.toFixed(2)} fautes` value: `Moyenne : ${stats?.avg.toFixed(2)} fautes`
} }
: null} : null}
/> />
</div> </div>
<div> <div>
<h3 style:text-align="right">Top essais</h3> <h3 style:text-align="right">Top essais</h3>
<Classement <Classement
tops={$parcours.tops.map((t) => { tops={$parcours.tops.map((t) => {
return { return {
name: t.challenger.name, name: t.challenger.name,
value: `${t.mistakes} fautes en ${parseTimer(t.time)}` value: `${t.mistakes} fautes en ${parseTimer(t.time)}`
}; };
})} })}
rank={$parcours.rank != null && $parcours.rank > 3 && $member != null rank={$parcours.rank != null && $parcours.rank > 3 && $member != null
? { ? {
name: $member.username, name: $member.username,
rank: $parcours.rank, rank: $parcours.rank,
value: `${$parcours.pb.mistakes} fautes en ${parseTimer($parcours.pb.time)}` value: `${$parcours.pb.mistakes} fautes en ${parseTimer($parcours.pb.time)}`
} }
: null} : null}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.stats { @import "../../mixins.scss";
display: flex;
flex-direction: column; .stats {
align-items: center; display: flex;
width: 100%; flex-direction: column;
gap: 20px; align-items: center;
} width: 100%;
.statistics { gap: 20px;
display: flex; }
gap: 20px;
p {
display: flex;
flex-direction: column;
align-items: center;
}
}
h1 { .statistics {
font-size: 3em; display: flex;
color: $red; gap: 20px;
text-align: center;
} p {
.validated { display: flex;
color: $green; flex-direction: column;
} align-items: center;
.ranks {
display: flex;
justify-content: space-evenly;
}
.classement {
width: 100%;
h2{
text-align: center;
}
} }
}
h1 {
font-size: 3em;
color: $red;
text-align: center;
}
.validated {
color: $green;
}
.ranks {
display: flex;
justify-content: space-evenly;
@include down(750) {
flex-direction: column;
align-items: center;
gap: 10px;
& > div {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
}
}
}
.classement {
width: 100%;
h2 {
text-align: center;
}
}
</style> </style>

View File

@ -94,7 +94,7 @@
if (checkExpire(exp) && refresh != null) { if (checkExpire(exp) && refresh != null) {
refreshRequest(refresh).then((r) => { refreshRequest(refresh).then((r) => {
localStorage.setItem('token', r.access_token); localStorage.setItem('token', r.access_token);
$username = username; $username = name;
$isAuth = true; $isAuth = true;
}); });

View File

@ -69,16 +69,19 @@
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 50%; //width: 50%;
transform: translateX(-50%) translateY(-50%) scale(0.7); transform: translateX(-50%) translateY(-50%) scale(0.7);
visibility: hidden; visibility: hidden;
transition: 0.4s; transition: 0.4s;
opacity: 0; opacity: 0;
z-index: 1000; z-index: 1000;
height: 57vh; max-height: 57vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center;
&.visible { &.visible {
visibility: visible !important; visibility: visible !important;
transform: translateX(-50%) translateY(-50%) scale(1) !important; transform: translateX(-50%) translateY(-50%) scale(1) !important;
@ -95,7 +98,7 @@
.overlay { .overlay {
background-color: black; background-color: black;
opacity: 0; opacity: 0;
z-index: 999; z-index: 500;
width: 100%; width: 100%;
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -1,160 +1,174 @@
<script lang="ts"> <script lang="ts">
import { setContext } from 'svelte'; import { setContext } from "svelte";
import { writable } from 'svelte/store'; import { writable } from "svelte/store";
import SvelteMarkdown from 'svelte-markdown' import SvelteMarkdown from "svelte-markdown";
type Notif = {
title: string;
description: string;
type: 'alert' | 'info' | 'success' | 'error';
};
type Notification = {
title: string;
description: string;
type: 'alert' | 'info' | 'success' | 'error';
id: number;
deleted: boolean;
};
const notifications = writable<Notification[]>([]); type Notif = {
title: string;
description: string;
type: "alert" | "info" | "success" | "error";
};
type Notification = {
title: string;
description: string;
type: "alert" | "info" | "success" | "error";
id: number;
deleted: boolean;
};
const getId = () => { const notifications = writable<Notification[]>([]);
return Math.round(Date.now() * Math.random());
};
const toast = (notif: Notif) => { const getId = () => {
let id = getId(); return Math.round(Date.now() * Math.random());
notifications.update((n) => { };
return [...n, { ...notif, id, deleted: false }];
});
setTimeout(() => {
notifications.update((n) => {
return n.map((o) => {
if (o.id == id) {
return { ...o, deleted: true };
}
return o;
});
});
setTimeout(() => {
notifications.update((n) => {
return n.filter((o) => o.id != id);
});
}, 500);
}, 3000);
};
const alert = (title: string, description: string) => { const toast = (notif: Notif) => {
toast({ title, description, type: 'alert' }); let id = getId();
}; notifications.update((n) => {
const info = (title: string, description: string) => { return [...n, { ...notif, id, deleted: false }];
toast({ title, description, type: 'info' }); });
}; setTimeout(() => {
const success = (title: string, description: string) => { notifications.update((n) => {
toast({ title, description, type: 'success' }); return n.map((o) => {
}; if (o.id == id) {
const error = (title: string, description: string) => { return { ...o, deleted: true };
toast({ title, description, type: 'error' }); }
}; return o;
setContext('notif', { toast, alert, info, success, error }); });
});
setTimeout(() => {
notifications.update((n) => {
return n.filter((o) => o.id != id);
});
}, 500);
}, 3000);
};
const alert = (title: string, description: string) => {
toast({ title, description, type: "alert" });
};
const info = (title: string, description: string) => {
toast({ title, description, type: "info" });
};
const success = (title: string, description: string) => {
toast({ title, description, type: "success" });
};
const error = (title: string, description: string) => {
toast({ title, description, type: "error" });
};
setContext("notif", { toast, alert, info, success, error });
</script> </script>
<slot /> <slot />
<div class="notifs" class:empty ={$notifications.length == 0}> <div class="notifs" class:empty={$notifications.length == 0}>
{#each $notifications as n} {#each $notifications as n}
<div <div
on:click={() => { on:click={() => {
n.deleted = true; n.deleted = true;
setTimeout(() => { setTimeout(() => {
notifications.update((o) => o.filter((i) => i.id != n.id)); notifications.update((o) => o.filter((i) => i.id != n.id));
}, 500); }, 500);
}} }}
class={n.type} class={n.type}
on:keydown={() => {}} on:keydown={() => {}}
class:deleted={n.deleted} class:deleted={n.deleted}
> >
<h1>{n.title}</h1> <h1>{n.title}</h1>
<p><SvelteMarkdown source={n.description} /></p> <p>
</div> <SvelteMarkdown source={n.description} />
{/each} </p>
</div>
{/each}
</div> </div>
<style lang="scss"> <style lang="scss">
.notifs { .notifs {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
padding: 30px; margin: 30px;
gap: 10px; gap: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 500px; width: min(calc(100% - 60px), 500px);
z-index: 1000; z-index: 500;
div { div {
background-color: $background; background-color: $background;
z-index: 1000; z-index: 1000;
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
word-wrap: break-word; word-wrap: break-word;
h1 {
font-size: 1.1em;
}
p {
font-size: 0.9em;
}
&::before, h1 {
&::after { font-size: 1.1em;
content: ''; }
display: block;
height: 3px;
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
&::before {
z-index: 3;
background-color: $background-light;
transition: 3s;
width: 0%;
animation: slide 3s forwards ease-in-out;
}
&::after {
width: 100%;
z-index: 2; p {
} font-size: 0.9em;
&.deleted { }
transition: 0.5s;
opacity: 0; &::before,
} &::after {
} content: '';
} display: block;
.empty{ height: 3px;
z-index: -20; position: absolute;
} bottom: 0;
.alert::after{ right: 0;
background-color: $orange; left: 0;
}
&::before {
z-index: 3;
background-color: $background-light;
transition: 3s;
width: 0%;
animation: slide 3s forwards ease-in-out;
}
&::after {
width: 100%;
z-index: 2;
}
&.deleted {
transition: 0.5s;
opacity: 0;
}
} }
.info::after{ }
background-color: $bleu;
.empty {
z-index: -20;
}
.alert::after {
background-color: $orange;
}
.info::after {
background-color: $bleu;
}
.error::after {
background-color: $red;
}
.success::after {
background-color: $green;
}
@keyframes slide {
from {
width: 0%;
} }
.error::after{ to {
background-color: $red; width: 100%;
} }
.success::after{ }
background-color: $green;
}
@keyframes slide {
from {
width: 0%;
}
to {
width: 100%;
}
}
</style> </style>

View File

@ -1,10 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import {authInstance} from '../apis/auth.api'; import {authInstance} from '../apis/auth.api';
import { env } from "$env/dynamic/public";
export const loginRequest = (data: { username: string; password: string }) => { export const loginRequest = (data: { username: string; password: string }) => {
return authInstance return authInstance
.request({ .request({
url: 'http://localhost:8002/login', url: '/login',
method: 'POST', method: 'POST',
data, data,
headers: { headers: {
@ -23,7 +24,7 @@ export const registerRequest = (data: {
}) => { }) => {
return authInstance return authInstance
.request({ .request({
url: 'http://localhost:8002/register', url: '/register',
method: 'POST', method: 'POST',
data, data,
headers: { headers: {
@ -37,9 +38,9 @@ export const registerRequest = (data: {
}; };
export const refreshRequest = (token: string) => { export const refreshRequest = (token: string) => {
return authInstance return axios
.request({ .request({
url: 'http://localhost:8002/refresh', url: `${env.PUBLIC_API_BASE}/refresh`,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',

View File

@ -131,9 +131,11 @@ export const getExoSource = (id_code: string,) => {
link.click(); link.click();
link.remove(); link.remove();
}); });
};export const generateRequest = (id_code: string,filename: string) => { };
export const generateRequest = (id_code: string,filename: string) => {
return exoInstance({ return exoInstance({
url: `/generator/csv/${id_code}/`, url: `generator/csv/${id_code}`,
method: 'Get', method: 'Get',
params: {filename} params: {filename}
}).then((r) => { }).then((r) => {

View File

@ -4,7 +4,7 @@ import { roomInstance } from '../apis/room.api';
export const createRoom = (data: { name: string }, username: string | null = null) => { export const createRoom = (data: { name: string }, username: string | null = null) => {
return roomInstance return roomInstance
.request({ .request({
url: '/', url: '',
method: 'POST', method: 'POST',
params: { username }, params: { username },
data: { ...data, public: false, global_results: false } data: { ...data, public: false, global_results: false }
@ -13,7 +13,6 @@ export const createRoom = (data: { name: string }, username: string | null = nul
}; };
export const getRoom = (id_code: string, clientId: string | null = null) => { export const getRoom = (id_code: string, clientId: string | null = null) => {
console.log('GETROOM', clientId, { ...(clientId != null && { clientId }) });
return roomInstance return roomInstance
.request({ .request({
url: '/' + id_code, url: '/' + id_code,
@ -23,6 +22,16 @@ export const getRoom = (id_code: string, clientId: string | null = null) => {
.then((r) => r.data); .then((r) => r.data);
}; };
export const deleteRoom = (id_code: string, clientId: string | null = null) => {
return roomInstance
.request({
url: '/' + id_code,
method: 'DELETE',
params: { ...(clientId != null && { clientId }) }
})
.then((r) => r.data);
};
export const createParcours = ( export const createParcours = (
id_code: string, id_code: string,
parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] }, parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] },

View File

@ -21,6 +21,7 @@
<Auth> <Auth>
<Alert> <Alert>
<Modal> <Modal>
<main> <main>
<NavBar/> <NavBar/>
<slot/> <slot/>
@ -78,7 +79,7 @@
height: calc(100vh - var(--navbar-height) - 10px); height: calc(100vh - var(--navbar-height) - 10px);
overflow: auto; overflow: auto;
height: 100%; height: 100%;
overflow-x: hidden;
a { a {
color: red; color: red;
} }

View File

@ -1,10 +1,30 @@
<script> <script lang="ts">
import {goto} from "$app/navigation";
</script> </script>
<button <div class="rooms">
on:click={() => { <h1>Générateur d'exercices</h1>
}}>test</button <div class="btns">
> <button class="primary-btn" on:click={()=>{goto('/exercices')}}>Générer</button>
<button class="primary-btn" on:click={()=>{goto('/room')}}>Salles en ligne</button>
</div>
</div>
<style lang="scss">
h1{
font-size: 4rem;
margin-bottom: 1rem;
}
.rooms {
display: flex;
flex-direction: column;
align-items: center;
}
.btns {
display: flex;
gap: 20px;
}
</style>

View File

@ -1,42 +1,42 @@
<script lang='ts'> <script lang="ts">
import {getContext} from "svelte"; import { getContext } from "svelte";
import {dashBoardRequest} from "../../requests/auth.request"; import { dashBoardRequest } from "../../requests/auth.request";
import Section from "../../components/auth/Section.svelte"; import Section from "../../components/auth/Section.svelte";
import InfoForm from "../../components/auth/InfoForm.svelte"; import InfoForm from "../../components/auth/InfoForm.svelte";
import RoomList from "../../components/auth/RoomList.svelte"; import RoomList from "../../components/auth/RoomList.svelte";
import type {User} from "../../types/auth.type"; import type { User } from "../../types/auth.type";
import type {Writable} from "svelte/store"; import type { Writable } from "svelte/store";
import {writable} from "svelte/store"; import { writable } from "svelte/store";
import PasswordForm from "../../components/auth/PasswordForm.svelte"; import PasswordForm from "../../components/auth/PasswordForm.svelte";
import {updatePassword, updateUserRequest} from "../../requests/auth.request.js"; import { updatePassword, updateUserRequest } from "../../requests/auth.request.js";
import UserConfirm from "../../components/auth/UserConfirm.svelte"; import UserConfirm from "../../components/auth/UserConfirm.svelte";
import MdInfo from 'svelte-icons/md/MdInfo.svelte' import MdInfo from "svelte-icons/md/MdInfo.svelte";
import FaUsers from 'svelte-icons/fa/FaUsers.svelte' import FaUsers from "svelte-icons/fa/FaUsers.svelte";
import FaUserLock from 'svelte-icons/fa/FaUserLock.svelte' import FaUserLock from "svelte-icons/fa/FaUserLock.svelte";
import {goto} from "$app/navigation"; import { goto } from "$app/navigation";
let u = ''; let u = "";
let p = ''; let p = "";
const user: Writable<User | null> = writable(null) const user: Writable<User | null> = writable(null);
const {logout, username, isAuth,initialLoading} = getContext('auth') const { logout, username, isAuth, initialLoading } = getContext("auth");
$: !$initialLoading && $isAuth && dashBoardRequest().then((res) => { $: !$initialLoading && $isAuth && dashBoardRequest().then((res) => {
console.log(res) console.log(res);
user.set(res) user.set(res);
}) });
const {show, close} = getContext("modal") const { show, close } = getContext("modal");
const {success, error} = getContext("notif") const { success, error } = getContext("notif");
let passwordForm = null let passwordForm = null;
let infoForm = null let infoForm = null;
$: !$initialLoading && !$isAuth && goto('/') $: !$initialLoading && !$isAuth && goto("/");
</script> </script>
<div> <div>
{#if $user != null} {#if $user != null}
<h1>Mon compte</h1> <h1>Mon compte</h1>
<Section title="Mes informations" icon={MdInfo} onValidate={()=>{ <Section title="Mes informations" icon={MdInfo} onValidate={()=>{
if(infoForm == null) return if(infoForm == null) return
updateUserRequest($infoForm.summary).then(res=>{ updateUserRequest($infoForm.summary).then(res=>{
user.update((o)=>{return {...o,...res}}) user.update((o)=>{return {...o,...res}})
@ -46,12 +46,12 @@
}) })
}} canValid={infoForm != null && $infoForm.valid}> }} canValid={infoForm != null && $infoForm.valid}>
<InfoForm user={$user} bind:myForm={infoForm}/> <InfoForm user={$user} bind:myForm={infoForm} />
</Section> </Section>
<Section title="Salles" icon={FaUsers}> <Section title="Salles" icon={FaUsers}>
<RoomList rooms={$user.rooms}/> <RoomList rooms={$user.rooms} />
</Section> </Section>
<Section title="Sécurité" icon={FaUserLock} validate="Modifier mon mot de passe" onValidate={()=>{ <Section title="Sécurité" icon={FaUserLock} validate="Modifier mon mot de passe" onValidate={()=>{
show(UserConfirm, {onValidate: ( p )=>{ show(UserConfirm, {onValidate: ( p )=>{
updatePassword({...$passwordForm.summary, old_password: p}).then((r)=>{ updatePassword({...$passwordForm.summary, old_password: p}).then((r)=>{
localStorage.setItem("token", r.access_token) localStorage.setItem("token", r.access_token)
@ -65,15 +65,24 @@
}, validate: "Changer mon mot de passe", cancel: close, cancelMsg: "Garder le mot de passe actuel"}) }, validate: "Changer mon mot de passe", cancel: close, cancelMsg: "Garder le mot de passe actuel"})
}} canValid={passwordForm != null && $passwordForm.valid}> }} canValid={passwordForm != null && $passwordForm.valid}>
<PasswordForm bind:myForm={passwordForm}/> <PasswordForm bind:myForm={passwordForm} />
</Section> </Section>
{/if} {/if}
</div> </div>
<style lang='scss'> <style lang="scss">
@import "../../mixins.scss";
h1 { h1 {
font-size: 3em; font-size: 3em;
margin-bottom: 20px; margin-bottom: 20px;
@include down(800){
text-align: center;
}
}
div {
padding: 7px 15px;
} }
</style> </style>

View File

@ -1,16 +1,31 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation"; import {goto} from "$app/navigation";
</script> </script>
<div> <div class="rooms">
<h1>Salles</h1> <h1>Salles</h1>
<div> <div class="btns">
<button class="primary-btn" on:click={()=>{goto('/room/join')}}>Rejoindre</button> <button class="primary-btn" on:click={()=>{goto('/room/join')}}>Rejoindre</button>
<button class="primary-btn" on:click={()=>{goto('/room/create')}}>Créer</button> <button class="primary-btn" on:click={()=>{goto('/room/create')}}>Créer</button>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
h1{
font-size: 4rem;
margin-bottom: 1rem;
}
.rooms {
display: flex;
flex-direction: column;
align-items: center;
}
.btns {
display: flex;
gap: 20px;
}
</style> </style>

View File

@ -26,13 +26,13 @@
const room = writable<Room | null>(null); const room = writable<Room | null>(null);
const member = writable<Member | null>(null); const member = writable<Member | null>(null);
const parcours = writable<ParcoursRead | null>(null); const parcours = writable<ParcoursRead | null>(null);
const ws = connect("ws://127.0.0.1:8002/ws/room/" + $page.params.slug); const ws = connect("ws://127.0.0.1:8002/api/ws/room/" + $page.params.slug);
setContext("room", room); setContext("room", room);
setContext("member", member); setContext("member", member);
setContext("parcours", parcours); setContext("parcours", parcours);
setContext("ws", { send: (type: string, data: Object) => ws.send({ type, data }), ws: ws.ws }); setContext("ws", { send: (type: string, data: Object) => ws.send({ type, data }), ws: ws.ws });
const { error } = getContext("notif"); const { error, info } = getContext("notif");
const onMessage = (payload: { type: string; data: any }) => { const onMessage = (payload: { type: string; data: any }) => {
if (payload == undefined) return; if (payload == undefined) return;
const { type, data } = payload; const { type, data } = payload;
@ -63,7 +63,6 @@
if ($page.url.searchParams.get("a") == "waiting") { if ($page.url.searchParams.get("a") == "waiting") {
goto(`?`); goto(`?`);
} }
console.log("ACCEPTED", data.member);
member.set(data.member); member.set(data.member);
getRoom($page.params.slug, !$isAuth ? data.member.clientId : null).then((r) => { getRoom($page.params.slug, !$isAuth ? data.member.clientId : null).then((r) => {
@ -83,11 +82,13 @@
case "waiting": case "waiting":
close();
$member = { ...data.waiter, room: data.room }; $member = { ...data.waiter, room: data.room };
goto(`?${new URLSearchParams({ a: "waiting" })}`); goto(`?${new URLSearchParams({ a: "waiting" })}`);
return; return;
case "refused": case "refused":
error("Refusé", "L'administrateur a refusé votre demande");
close(); close();
ws?.close(1000); ws?.close(1000);
goto("/room/join"); goto("/room/join");
@ -101,17 +102,25 @@
return; return;
} }
if ($room != null) { if ($room != null) {
info("Départ", `*${data.member.username}* n'est plus dans la salle`);
$room.members = [ $room.members = [
...$room.members.filter((r) => "waiter_id" in r || r.id_code != data.member.id_code) ...$room.members.filter((r) => "waiter_id" in r || r.id_code != data.member.id_code)
]; ];
} }
return; return;
case "banned": case "banned":
error("Ban", "Vous avez été banni de la salle par l'administrateur");
ws?.close(1000); ws?.close(1000);
goto("/room/join"); goto("/room/join");
sessionStorage.removeItem("reconnect"); sessionStorage.removeItem("reconnect");
return; return;
case "deleted":
info("Suppression", "La salle a été supprimée par l'administrateur");
ws.close(1000);
goto("/room/join");
return;
case "error": case "error":
const { code, msg } = data; const { code, msg } = data;
if (code == 401) { if (code == 401) {
@ -148,6 +157,10 @@
} else { } else {
error("Erreur", "Message : " + msg); error("Erreur", "Message : " + msg);
} }
if (code == 404) {
ws.close(1000);
goto("/room/join");
}
return; return;
case "waiter": case "waiter":
@ -199,6 +212,7 @@
case "joined": case "joined":
if ($room != null) { if ($room != null) {
info("Arrivée", `*${data.member.username}* a rejoint la salle`)
$room.members = [ $room.members = [
...$room?.members.filter( ...$room?.members.filter(
(m) => (m) =>
@ -346,8 +360,9 @@
$parcours.validated = $parcours.pb.mistakes <= $parcours.max_mistakes; $parcours.validated = $parcours.pb.mistakes <= $parcours.max_mistakes;
} }
} }
return
case "challenge_change": case "challenge_change":
if ($parcours != null && !!$parcours.challenges[data.member]) { if ($parcours != null && $member != null && !!$parcours.challenges[data.member]) {
$parcours.challenges[data.member].challenges = $parcours.challenges[ $parcours.challenges[data.member].challenges = $parcours.challenges[
data.member data.member
].challenges.map((c) => { ].challenges.map((c) => {
@ -356,8 +371,24 @@
} }
return c; return c;
}); });
$parcours.validated = data.validated; $parcours.challenges[data.member].challenger.validated = data.validated;
if (data.member == $member.id_code) {
$parcours.validated = data.validated;
}
} }
return;
case "update_challenges":
if ($parcours != null && $member != null) {
const { challenger, challenges } = data;
if (challenges.length != 0) {
$parcours.challenges[challenger.id_code] = data;
}
if (challenger.id_code == $member.id_code) {
$parcours.validated = data.challenger.validated;
}
}
return;
} }
}; };
@ -419,6 +450,7 @@
$: console.log("edit", $page.url.searchParams.get("a")); $: console.log("edit", $page.url.searchParams.get("a"));
onDestroy(() => { onDestroy(() => {
ws.close(1000); ws.close(1000);
}); });
@ -507,6 +539,7 @@
column-gap: 50px; column-gap: 50px;
row-gap: 10px; row-gap: 10px;
height: 100%; height: 100%;
padding: 7px 15px;
@include down(800) { @include down(800) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -7,6 +7,8 @@
let name = ""; let name = "";
let pseudo = ""; let pseudo = "";
const { isAuth } = getContext("auth"); const { isAuth } = getContext("auth");
let loading = false;
const { error } = getContext("notif");
</script> </script>
<div class="container"> <div class="container">
@ -20,15 +22,27 @@
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
console.log('(NAME)', name) console.log('(NAME)', name)
loading = true
createRoom({ name }, !$isAuth ? pseudo : null).then((r) => { createRoom({ name }, !$isAuth ? pseudo : null).then((r) => {
if(!$isAuth){ if(!$isAuth){
sessionStorage.setItem('reconnect', r.member) sessionStorage.setItem('reconnect', r.member)
} }
goto(`/room/${r.room}`); goto(`/room/${r.room}`);
}); loading= false
}).catch((e) => {
error("Erreur", "Une erreur est survenue lors de la création de la salle")
loading= false
console.log(e);
});
}} }}
> >
Valider {#if loading}
<span class="spinner"></span>
{:else}
Créer
{/if}
</button> </button>
</div> </div>
</div> </div>
@ -51,4 +65,10 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.spinner {
width: 15px;
height: 15px;
border-width: 2px !important;
}
</style> </style>

View File

@ -4,7 +4,6 @@
let room = ""; let room = "";
</script> </script>
<div class="container"> <div class="container">

View File

@ -11,6 +11,7 @@
$: { $: {
!$initialLoading && !!$isAuth && goto('/dashboard'); !$initialLoading && !!$isAuth && goto('/dashboard');
} }
let loading = false
</script> </script>
<div class="parent"> <div class="parent">
@ -37,13 +38,19 @@
<button <button
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
login(username, password).catch((r) => { loading = true;
login(username, password).then(()=>{loading=false}).catch((r) => {
errors = { ...errors, ...r.data.detail }; errors = { ...errors, ...r.data.detail };
loading = false;
}); });
}} }}
> >
Se connecter {#if loading}
<span class="spinner" />
{:else}
Se connecter
{/if}
</button> </button>
</div> </div>
</div> </div>
@ -65,4 +72,9 @@
gap: 20px; gap: 20px;
} }
} }
.spinner{
width: 15px;
height: 15px;
border-width: 2px!important;
}
</style> </style>

View File

@ -18,6 +18,7 @@
const myForm = form(username, password, confirm); const myForm = form(username, password, confirm);
$: !$initialLoading && !!$isAuth && goto('/dashboard'); $: !$initialLoading && !!$isAuth && goto('/dashboard');
let loading = false
</script> </script>
<div class="parent"> <div class="parent">
@ -62,7 +63,9 @@
<button <button
class="primary-btn" class="primary-btn"
on:click={() => { on:click={() => {
register($username.value, $password.value, $confirm.value).catch((r) => { loading = true
register($username.value, $password.value, $confirm.value).then(()=>{loading=false}).catch((r) => {
loading = false
console.log('ERREUR', r); console.log('ERREUR', r);
errors = { ...errors, ...r.data.detail }; errors = { ...errors, ...r.data.detail };
@ -70,7 +73,12 @@
}} }}
disabled={!$myForm.valid} disabled={!$myForm.valid}
> >
S'inscrire <!-- loading et span.spinner else text-->
{#if loading}
<span class="spinner"></span>
{:else}
S'inscrire
{/if}
</button> </button>
</div> </div>
</div> </div>
@ -92,4 +100,9 @@
gap: 20px; gap: 20px;
} }
} }
.spinner{
width: 15px;
height: 15px;
border-width: 2px!important;
}
</style> </style>

View File

@ -9,7 +9,6 @@ export const connect = (url,) => {
const ws = new ReconnectingWebSocket(url); const ws = new ReconnectingWebSocket(url);
ws.onmessage = (m) => { ws.onmessage = (m) => {
console.log('MESAGE', m)
messages.update((o) => [JSON.parse(m.data), ...o]); messages.update((o) => [JSON.parse(m.data), ...o]);
Object.values(get(handlers)).map(h => { Object.values(get(handlers)).map(h => {