Compare commits
4 Commits
90b48e710f
...
8c83658708
Author | SHA1 | Date | |
---|---|---|---|
|
8c83658708 | ||
2dc81bb4e3 | |||
71038c169c | |||
|
9393c88f8e |
@ -5,7 +5,7 @@ from typing import List
|
||||
from fastapi import Depends, HTTPException, status, Query
|
||||
from pydantic import BaseModel
|
||||
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.models import User
|
||||
@ -59,9 +59,15 @@ def change_room_status(room: Room, public: bool, db: Session):
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
@ -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):
|
||||
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
|
||||
|
||||
|
||||
@ -109,7 +115,8 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
|
||||
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 = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
|
||||
id_code=member_id)
|
||||
@ -154,7 +161,7 @@ def disconnect_member(member: Member, db: Session):
|
||||
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:
|
||||
return None
|
||||
members = select(Member.anonymous_id).where(
|
||||
@ -269,6 +276,8 @@ def refuse_waiter(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.commit()
|
||||
return None
|
||||
@ -324,6 +333,12 @@ def getChallenges(c: Challenger, db: Session):
|
||||
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):
|
||||
tops = db.exec(select(Challenge).where(Challenge.parcours_id == p.id_code).order_by(
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
tops = getTops(parcours, db)
|
||||
avgTop = getAvgTops(parcours, db)
|
||||
@ -392,55 +414,13 @@ def serialize_parcours(parcours: Parcours, member: Member, db: Session):
|
||||
challengers = db.exec(statement).all()
|
||||
|
||||
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
|
||||
"challenges": [Challenges(**{**chall.dict(), "canCorrige": chall.data != []}) for chall in getChallenges(c, db)]
|
||||
} for c in challengers}
|
||||
|
||||
return {**parcours.dict(), "pb": pb, "tops": tops, "challenges": challs, "rank": noteRank, "memberRank": avgRank,
|
||||
"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):
|
||||
@ -510,29 +490,23 @@ def deleteParcoursRelated(parcours: Parcours, db: Session):
|
||||
db.commit()
|
||||
|
||||
|
||||
def change_challengers_validation(p: Parcours, validation: int, db: Session):
|
||||
challengers = db.exec(select(Challenger).where(
|
||||
Challenger.parcours_id == p.id)).all()
|
||||
challs = []
|
||||
for c in challengers:
|
||||
validated = c.best <= validation
|
||||
if validated != c.validated:
|
||||
c.validated = validated
|
||||
challs.append(c)
|
||||
|
||||
db.bulk_save_objects(challs)
|
||||
def change_challengers_validation(p: Parcours, db: Session):
|
||||
stmt = update(Challenger).values(
|
||||
validated=select(Challenge.id).where(Challenge.challenger_mid == Challenger.member_id,
|
||||
Challenge.challenger_pid == Challenger.parcours_id,
|
||||
Challenge.validated == 1).exists()).where(
|
||||
Challenger.parcours_id == p.id)
|
||||
db.execute(stmt)
|
||||
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(
|
||||
Challenge.parcours_id == p.id_code)).all()
|
||||
print('CHALLS', challenges)
|
||||
challs = []
|
||||
for c in challenges:
|
||||
validated = c.mistakes <= validation
|
||||
print('CHAL', validated, c.validated, c)
|
||||
validated = c.time <= p.time * 60 and c.mistakes <= p.max_mistakes
|
||||
if validated != c.validated:
|
||||
c.validated = validated
|
||||
challs.append(c)
|
||||
@ -541,9 +515,10 @@ def change_challenges_validation(p: Parcours, validation: int, db: Session):
|
||||
db.commit()
|
||||
|
||||
|
||||
def changeValidation(p: Parcours, validation: int, db: Session):
|
||||
change_challengers_validation(p, validation, db)
|
||||
change_challenges_validation(p, validation, db)
|
||||
def changeValidation(p: Parcours, db: Session):
|
||||
change_challenges_validation(p, db)
|
||||
|
||||
change_challengers_validation(p, db)
|
||||
|
||||
|
||||
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
|
||||
parcours_obj.exercices = exercices
|
||||
|
||||
if parcours_obj.max_mistakes != parcours.max_mistakes:
|
||||
changeValidation(parcours_obj, parcours.max_mistakes, db)
|
||||
|
||||
update_validated = parcours_obj.max_mistakes != parcours.max_mistakes or parcours_obj.time != parcours.time
|
||||
parcours_obj.name = parcours.name
|
||||
parcours_obj.time = parcours.time
|
||||
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.refresh(parcours_obj)
|
||||
if update_validated:
|
||||
changeValidation(parcours_obj, db)
|
||||
|
||||
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,
|
||||
isCorriged: bool, db: Session):
|
||||
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,
|
||||
parcours=parcours, time=time, mistakes=mistakes, isCorriged=isCorriged,
|
||||
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_time = challenge.time
|
||||
|
||||
validated = corriged['mistakes'] <= parcours.max_mistakes
|
||||
validated = challenge.time <= parcours.time * 60 and corriged['mistakes'] <= parcours.max_mistakes
|
||||
challenge.validated = 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)):
|
||||
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:
|
||||
raise HTTPException(
|
||||
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_(
|
||||
[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(
|
||||
TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first()
|
||||
if tmpCorr is None:
|
||||
|
@ -24,8 +24,8 @@ class Room(RoomBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
id_code: str = Field(index=True)
|
||||
|
||||
members: List['Member'] = Relationship(back_populates="room")
|
||||
parcours: List['Parcours'] = 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",sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
|
||||
|
||||
|
||||
class AnonymousBase(SQLModel):
|
||||
@ -58,7 +58,9 @@ class Member(SQLModel, table=True):
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
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
|
||||
|
||||
@ -68,7 +70,6 @@ class Member(SQLModel, table=True):
|
||||
|
||||
waiter_code: Optional[str] = Field(default=None)
|
||||
|
||||
corrections: List['TmpCorrection'] = Relationship(back_populates="member")
|
||||
|
||||
|
||||
class ExercicesCreate(SQLModel):
|
||||
@ -101,7 +102,7 @@ class Parcours(SQLModel, table=True):
|
||||
room_id: int = Field(foreign_key="room.id")
|
||||
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
|
||||
time: int
|
||||
@ -110,9 +111,9 @@ class Parcours(SQLModel, table=True):
|
||||
max_mistakes: int
|
||||
|
||||
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(
|
||||
back_populates="parcours")
|
||||
back_populates="parcours", sa_relationship_kwargs={"cascade": "all, delete, delete-orphan"})
|
||||
|
||||
|
||||
|
||||
@ -144,6 +145,7 @@ class ParcoursReadUpdate(SQLModel):
|
||||
class ChallengerInfo(BaseModel):
|
||||
name: str
|
||||
id_code: str
|
||||
validated: bool = False
|
||||
|
||||
|
||||
class ChallengerAverage(ChallengerInfo):
|
||||
|
Binary file not shown.
@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, Query, UploadFile, HTTPException, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
from sqlmodel import Session, select, col
|
||||
|
||||
from database.auth.models import User
|
||||
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(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
statement = statement.where(sub)
|
||||
|
||||
statement = statement.order_by(col(Exercice.id).desc())
|
||||
page = p(db, statement)
|
||||
exercices = 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(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
statement = statement.where(sub)
|
||||
|
||||
statement = statement.order_by(col(Exercice.id).desc())
|
||||
page = p(db, statement)
|
||||
print('¨PAGE', page)
|
||||
exercices = page.items
|
||||
|
@ -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, \
|
||||
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
|
||||
from database.room.models import Room, Member, MemberRead, Waiter
|
||||
from database.room.models import Room, Member, MemberRead, Waiter, Challenger
|
||||
from services.websocket import Consumer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
|
||||
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.ws = ws
|
||||
self.manager = manager
|
||||
@ -25,14 +25,22 @@ class RoomConsumer(Consumer):
|
||||
self.member = None
|
||||
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
|
||||
async def send(self, payload: Any | Callable):
|
||||
if callable(payload):
|
||||
payload = payload(self.member)
|
||||
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):
|
||||
sending = {'type': type, "data": payload, }
|
||||
@ -243,12 +251,13 @@ class RoomConsumer(Consumer):
|
||||
|
||||
@Consumer.event('leave', conditions=[isMember])
|
||||
async def leave(self):
|
||||
print('LEAVED', self.member, isinstance(self.member, Member), isinstance(self.member, Challenger))
|
||||
if self.member.is_admin is True:
|
||||
await self.send_error("Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur")
|
||||
return
|
||||
member_obj = serialize_member(self.member)
|
||||
leave_room(self.member, self.db)
|
||||
|
||||
self.member = None
|
||||
await self.direct_send(type="successfully_leaved", payload={})
|
||||
await self.broadcast(type='leaved', payload={"member": member_obj})
|
||||
self.member = None
|
||||
@ -294,13 +303,13 @@ class RoomConsumer(Consumer):
|
||||
self.manager.remove(self.room.id, self)
|
||||
return {"waiter_id": waiter_id}
|
||||
|
||||
@Consumer.sending("banned", conditions=[isMember])
|
||||
async def banned(self):
|
||||
self.member = None
|
||||
self.manager.remove(self.room.id_code, self)
|
||||
self.banned = True
|
||||
#await self.ws.close()
|
||||
return {}
|
||||
# @Consumer.sending("banned", conditions=[isMember])
|
||||
# def banned(self):
|
||||
# self.member = None
|
||||
# self.manager.remove(self.room.id_code, self)
|
||||
# self.banned = True
|
||||
# #await self.ws.close()
|
||||
# return {}
|
||||
|
||||
@Consumer.sending('ping', conditions=[isMember])
|
||||
def ping(self):
|
||||
|
@ -39,7 +39,6 @@ class RoomManager:
|
||||
|
||||
if group in self.active_connections:
|
||||
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):
|
||||
await self._send(connection, message, group)
|
||||
|
||||
|
@ -8,35 +8,12 @@ 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 delete_room_db, getUsername, getMemberValidated
|
||||
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, \
|
||||
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
|
||||
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
|
||||
@ -68,6 +45,14 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
|
||||
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)
|
||||
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin),
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
@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),
|
||||
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": {
|
||||
"parcours": ParcoursReadUpdate(**parcours_obj.dict(), update_challenges=update_challenges).dict()}},
|
||||
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 {**parcours_obj.dict()}
|
||||
|
||||
|
||||
@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)
|
||||
print('CHALLENGE', chall)
|
||||
db.delete(correction)
|
||||
returnValue = {**chall.dict(), 'validated': chall.mistakes <= correction.parcours.max_mistakes}
|
||||
returnValue = {**chall.dict()}
|
||||
db.commit()
|
||||
return returnValue
|
||||
# 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}')
|
||||
async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session),
|
||||
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)
|
||||
await consumer.run()
|
||||
|
@ -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
|
||||
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 _(name: str | List, conditions: List[Callable | bool] = []):
|
||||
@ -62,7 +63,8 @@ class Consumer:
|
||||
#self.events: Dict[str, Callable] = {}
|
||||
|
||||
async def connect(self):
|
||||
pass
|
||||
await self.ws.accept()
|
||||
return True
|
||||
|
||||
async def validation_error_handler(self, e: ValidationError):
|
||||
errors = e.errors()
|
||||
@ -132,7 +134,9 @@ class Consumer:
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
await self.connect()
|
||||
accepted = await self.connect()
|
||||
if accepted is False:
|
||||
return
|
||||
try:
|
||||
while True:
|
||||
data = await self.ws.receive_json()
|
||||
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -8,4 +8,5 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.idea
|
||||
.idea
|
||||
*/database*
|
||||
|
@ -1,17 +1,17 @@
|
||||
import axios from 'axios';
|
||||
import { autoRefresh } from '../utils/utils';
|
||||
import {env} from '$env/dynamic/private';
|
||||
import axios from "axios";
|
||||
import { autoRefresh } from "../utils/utils";
|
||||
import { env } from "$env/dynamic/public";
|
||||
|
||||
export const authInstance = axios.create({
|
||||
baseURL: `${env.API_BASE}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||
}
|
||||
baseURL: `${env.PUBLIC_API_BASE}`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||
}
|
||||
});
|
||||
|
||||
authInstance.interceptors.request.use(autoRefresh, (error) => {
|
||||
Promise.reject(error);
|
||||
Promise.reject(error);
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import { parse, stringify } from 'qs'
|
||||
import { autoRefresh } from '../utils/utils';
|
||||
import {env} from '$env/dynamic/private';
|
||||
import { env } from "$env/dynamic/public";
|
||||
export const exoInstance = axios.create({
|
||||
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: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
|
@ -1,20 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import { autoRefresh } from '../utils/utils';
|
||||
import {env} from '$env/dynamic/private';
|
||||
import axios from "axios";
|
||||
import { autoRefresh } from "../utils/utils";
|
||||
import { env } from "$env/dynamic/public";
|
||||
|
||||
export const roomInstance = axios.create({
|
||||
baseURL: `${env.API_BASE}/room`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||
}
|
||||
baseURL: `${env.PUBLIC_API_BASE}room`,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||
}
|
||||
});
|
||||
|
||||
roomInstance.interceptors.request.use(
|
||||
autoRefresh,
|
||||
(error) => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
autoRefresh,
|
||||
(error) => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
@ -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 */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 30px;
|
||||
height:30px;
|
||||
height: 30px;
|
||||
border: 3px solid $contrast;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
@ -14,14 +15,15 @@
|
||||
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
@ -34,95 +36,109 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
height: 38px;
|
||||
font-weight: 700;
|
||||
transition: 0.3s;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 7px;
|
||||
padding: 0 50px;
|
||||
width: max-content;
|
||||
cursor: pointer;
|
||||
&:disabled{
|
||||
cursor: not-allowed
|
||||
}
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
height: 38px;
|
||||
font-weight: 700;
|
||||
transition: 0.3s;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 7px;
|
||||
padding: 0 50px;
|
||||
width: max-content;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed
|
||||
}
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
@extend .btn;
|
||||
background-color: #fcbf49;
|
||||
&:hover {
|
||||
background-color: #ac7b19;
|
||||
}
|
||||
@extend .btn;
|
||||
background-color: #fcbf49;
|
||||
|
||||
&:hover {
|
||||
background-color: #ac7b19;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
@extend .btn;
|
||||
background-color: #fc5e49;
|
||||
&:hover {
|
||||
background-color: #ac1919;
|
||||
}
|
||||
@extend .btn;
|
||||
background-color: #fc5e49;
|
||||
|
||||
&:hover {
|
||||
background-color: #ac1919;
|
||||
}
|
||||
}
|
||||
|
||||
.border-primary-btn {
|
||||
@extend .btn;
|
||||
background-color: transparent;
|
||||
border: 1px solid #fcbf49;
|
||||
color: #fcbf49;
|
||||
&:hover {
|
||||
background-color: #fcbf49;
|
||||
color: black;
|
||||
}
|
||||
@extend .btn;
|
||||
background-color: transparent;
|
||||
border: 1px solid #fcbf49;
|
||||
color: #fcbf49;
|
||||
|
||||
&:hover {
|
||||
background-color: #fcbf49;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 450;
|
||||
margin: 10px 0;
|
||||
float: left;
|
||||
border: none;
|
||||
border-bottom: 1px solid #181553;
|
||||
transition: 0.3s;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom-color: $contrast;
|
||||
}
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 450;
|
||||
margin: 10px 0;
|
||||
float: left;
|
||||
border: none;
|
||||
border-bottom: 1px solid #181553;
|
||||
transition: 0.3s;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom-color: $contrast;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flex-row-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@for $f from 0 through 100 {
|
||||
.wp-#{$f} {
|
||||
width: 1% * $f;
|
||||
}
|
||||
.wp-#{$f} {
|
||||
width: 1% * $f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sv-dropdown{
|
||||
z-index: 10!important;
|
||||
.sv-dropdown {
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
.strong{
|
||||
font-weight: 900;
|
||||
.strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.contrast {
|
||||
color: $contrast;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
cursor: progress;
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
import FaHome from "svelte-icons/fa/FaHome.svelte";
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
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 {
|
||||
isAuth,
|
||||
@ -16,43 +16,46 @@
|
||||
afterNavigate(() => {
|
||||
open = false;
|
||||
});
|
||||
$: console.log("USERNAME", $username);
|
||||
</script>
|
||||
|
||||
<nav data-sveltekit-preload-data="hover" class:open>
|
||||
<div class="navigate">
|
||||
<NavLink href="/" exact no_hover class="home">
|
||||
<div class="icon">
|
||||
<FaHome />
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink href="/" exact no_hover class="home">
|
||||
<div class="icon">
|
||||
<FaHome />
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink href="/exercices">Exercices</NavLink>
|
||||
<NavLink href="/room">Salles</NavLink>
|
||||
|
||||
</div>
|
||||
<div class="auth">
|
||||
{#if $isAuth && $username != null}
|
||||
<div class="right">
|
||||
<div class="auth">
|
||||
{#if $isAuth && $username != null}
|
||||
|
||||
<NavLink href="/dashboard">
|
||||
<div class="dashboard">
|
||||
<div class="icon">
|
||||
<FaUser />
|
||||
<NavLink href="/dashboard">
|
||||
<div class="dashboard">
|
||||
<div class="icon">
|
||||
<FaUser />
|
||||
</div>
|
||||
{$username}
|
||||
</div>
|
||||
{$username}</div>
|
||||
</NavLink>
|
||||
<div class="icon signout" title="Se déconnecter" on:click={()=>{
|
||||
</NavLink>
|
||||
<div class="icon signout" title="Se déconnecter" on:click={()=>{
|
||||
logout()
|
||||
}}>
|
||||
<FaSignOutAlt />
|
||||
</div>
|
||||
<IoIosLogOut />
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<NavLink href="/signup" exact>S'inscrire</NavLink>
|
||||
<NavLink href="/signin" exact>Se connecter</NavLink>
|
||||
{/if}
|
||||
{:else}
|
||||
<NavLink href="/signup" exact>S'inscrire</NavLink>
|
||||
<NavLink href="/signin" exact>Se connecter</NavLink>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<div class="burger" on:click={()=>{open=!open}}><span> </span></div>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<style lang="scss">
|
||||
@import "../mixins";
|
||||
@ -107,9 +110,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.auth {
|
||||
transition: .3s;
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
@include down(666) {
|
||||
//display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.burger {
|
||||
background: 0 0;
|
||||
@include up(750px) {
|
||||
@include up(666) {
|
||||
display: none;
|
||||
}
|
||||
border: none;
|
||||
@ -164,15 +178,11 @@
|
||||
|
||||
.open {
|
||||
|
||||
@include down(750px) {
|
||||
@include down(666px) {
|
||||
.navigate {
|
||||
*:first-child {
|
||||
display: none
|
||||
}
|
||||
|
||||
max-height: 1000000px;
|
||||
|
||||
// Remove home icon
|
||||
transition: .2s;
|
||||
background: rgba($background-dark, 0.8);
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
@ -184,18 +194,31 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 42px;
|
||||
z-index: 100;
|
||||
//gap: 42px;
|
||||
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 {
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
z-index: 101;
|
||||
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& .burger {
|
||||
z-index: 1000;
|
||||
|
||||
& span::before {
|
||||
bottom: 0;
|
||||
@ -220,4 +243,14 @@
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
@keyframes open {
|
||||
0% {
|
||||
gap: 10px;
|
||||
opacity: .3;
|
||||
}
|
||||
100% {
|
||||
gap: 42px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,46 +1,51 @@
|
||||
<script lang="ts">
|
||||
import {field, form} from "svelte-forms";
|
||||
import {max, min, required, email} from "svelte-forms/validators";
|
||||
import LabeledInput from "../forms/LabeledInput.svelte";
|
||||
import type {User} from "../../types/auth.type";
|
||||
import {onMount} from "svelte";
|
||||
import {errorMsg} from "../../utils/forms.js";
|
||||
import { field, form } from "svelte-forms";
|
||||
import { max, min, required, email } from "svelte-forms/validators";
|
||||
import LabeledInput from "../forms/LabeledInput.svelte";
|
||||
import type { User } from "../../types/auth.type";
|
||||
import { onMount } from "svelte";
|
||||
import { errorMsg } from "../../utils/forms.js";
|
||||
|
||||
export let user: User
|
||||
export let myForm;
|
||||
const username = field('username', user.username, [required(), max(20), min(2)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const name = field('name', user.name || "", [max(50)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const firstname = field('firstname', user.firstname || "", [max(50),], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const emailField = field('email', user.email || "", [ /*email()*/], {
|
||||
checkOnInit: true
|
||||
});
|
||||
onMount(() => {
|
||||
myForm = form(username, name, firstname, emailField);
|
||||
})
|
||||
export let user: User;
|
||||
export let myForm;
|
||||
const username = field("username", user.username, [required(), max(20), min(2)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const name = field("name", user.name || "", [max(50)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const firstname = field("firstname", user.firstname || "", [max(50)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
const emailField = field("email", user.email || "", [ /*email()*/], {
|
||||
checkOnInit: true
|
||||
});
|
||||
onMount(() => {
|
||||
myForm = form(username, name, firstname, emailField);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !!$myForm}
|
||||
<div class="">
|
||||
<LabeledInput bind:value={$username.value} label="Nom d'utilisateur" type="text" placeholder="Nom d'utilisateur..."
|
||||
errors={errorMsg($myForm, 'username')}/>
|
||||
<LabeledInput bind:value={$emailField.value} label="Email" type="email" placeholder="Email..."
|
||||
errors={errorMsg($myForm, 'email')}/>
|
||||
<LabeledInput bind:value={$name.value} label="Nom" type="text" placeholder="Nom..."
|
||||
errors={errorMsg($myForm, 'name')}/>
|
||||
<LabeledInput bind:value={$firstname.value} label="Prénom" type="text" placeholder="Prénom..."
|
||||
errors={errorMsg($myForm, 'firstname')}/>
|
||||
</div>
|
||||
<div class="">
|
||||
<LabeledInput bind:value={$username.value} label="Nom d'utilisateur" type="text" placeholder="Nom d'utilisateur..."
|
||||
errors={errorMsg($myForm, 'username')} />
|
||||
<LabeledInput bind:value={$emailField.value} label="Email" type="email" placeholder="Email..."
|
||||
errors={errorMsg($myForm, 'email')} />
|
||||
<LabeledInput bind:value={$name.value} label="Nom" type="text" placeholder="Nom..."
|
||||
errors={errorMsg($myForm, 'name')} />
|
||||
<LabeledInput bind:value={$firstname.value} label="Prénom" type="text" placeholder="Prénom..."
|
||||
errors={errorMsg($myForm, 'firstname')} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../mixins.scss";
|
||||
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@include down(800px){
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,31 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {field, form} from "svelte-forms";
|
||||
import {matchField, min, pattern, required} from "svelte-forms/validators";
|
||||
import LabeledInput from "../forms/LabeledInput.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import {errorMsg} from "../../utils/forms";
|
||||
import { field, form } from "svelte-forms";
|
||||
import { matchField, min, pattern, required } from "svelte-forms/validators";
|
||||
import LabeledInput from "../forms/LabeledInput.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { errorMsg } from "../../utils/forms";
|
||||
|
||||
|
||||
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});
|
||||
export let myForm;
|
||||
onMount(() => {
|
||||
myForm = form(password, confirm)
|
||||
})
|
||||
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 });
|
||||
export let myForm;
|
||||
onMount(() => {
|
||||
myForm = form(password, confirm);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if !!$myForm}
|
||||
<div>
|
||||
<LabeledInput bind:value={$password.value} type="password" placeholder="Mot de passe..."
|
||||
errors={errorMsg($myForm, 'password')}/>
|
||||
<LabeledInput bind:value={$confirm.value} type="password" placeholder="Confirmer..."
|
||||
errors={errorMsg($myForm, 'password_confirm')}/>
|
||||
</div>
|
||||
<div>
|
||||
<LabeledInput bind:value={$password.value} type="password" placeholder="Mot de passe..."
|
||||
errors={errorMsg($myForm, 'password')} />
|
||||
<LabeledInput bind:value={$confirm.value} type="password" placeholder="Confirmer..."
|
||||
errors={errorMsg($myForm, 'password_confirm')} />
|
||||
</div>
|
||||
{/if}
|
||||
<style lang="scss">
|
||||
@import "../../mixins.scss";
|
||||
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@include down(800){
|
||||
grid-template-columns: 1fr;}
|
||||
}
|
||||
</style>
|
@ -10,7 +10,7 @@
|
||||
<ul>
|
||||
{#each rooms as room}
|
||||
<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>
|
||||
{/each}
|
||||
</ul>
|
||||
|
@ -1,34 +1,41 @@
|
||||
<script lang="ts">
|
||||
|
||||
export let icon = null
|
||||
export let title;
|
||||
export let validate = "Valider !"
|
||||
export let onValidate = null
|
||||
export let canValid = false
|
||||
export let icon = null;
|
||||
export let title;
|
||||
export let validate = "Valider !";
|
||||
export let onValidate = null;
|
||||
export let canValid = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h2>
|
||||
<div class="icon">
|
||||
<svelte:component this={icon}/></div>
|
||||
{title}</h2>
|
||||
<div class="content">
|
||||
<slot/>
|
||||
<h2>
|
||||
<div class="icon">
|
||||
<svelte:component this={icon} />
|
||||
</div>
|
||||
{title}</h2>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
{#if !!onValidate}
|
||||
<div class="btn-container">
|
||||
<button on:click={onValidate} class="primary-btn" disabled={!canValid}>{validate}</button>
|
||||
</div>
|
||||
{#if !!onValidate}
|
||||
<div class="btn-container">
|
||||
<button on:click={onValidate} class="primary-btn" disabled={!canValid}>{validate}</button>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../mixins.scss";
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include down(800){
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.icon{
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
@ -49,5 +56,8 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
@include down(800){
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,71 +1,71 @@
|
||||
<script lang="ts">
|
||||
import type { Exercice } from '../../types/exo.type';
|
||||
import { getContext } from 'svelte';
|
||||
import ModalCard from './ModalCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { cloneExo } from '../../requests/exo.request';
|
||||
import TagContainer from './TagContainer.svelte';
|
||||
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Exercice } from "../../types/exo.type";
|
||||
import { getContext } from "svelte";
|
||||
import ModalCard from "./ModalCard.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { cloneExo } from "../../requests/exo.request";
|
||||
import TagContainer from "./TagContainer.svelte";
|
||||
import PrivacyIndicator from "./PrivacyIndicator.svelte";
|
||||
import MdContentCopy from "svelte-icons/md/MdContentCopy.svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let exo: Exercice;
|
||||
export let exo: Exercice;
|
||||
|
||||
const { show } = getContext<{ show: Function }>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>('auth');
|
||||
const exerciceStore = getContext('exos');
|
||||
const tagsStore = getContext('tags');
|
||||
const { show } = getContext<{ show: Function }>("modal");
|
||||
const { navigate } = getContext<{ navigate: Function }>("navigation");
|
||||
const { isAuth } = getContext<{ isAuth: Writable<boolean> }>("auth");
|
||||
const exerciceStore = getContext("exos");
|
||||
const tagsStore = getContext("tags");
|
||||
|
||||
let opened = false;
|
||||
const handleClick = () => {
|
||||
opened = true;
|
||||
navigate(`/exercices/${exo.id_code}`);
|
||||
show(
|
||||
ModalCard,
|
||||
{
|
||||
exo,
|
||||
exos: exerciceStore,
|
||||
tags: tagsStore
|
||||
},
|
||||
() => {
|
||||
navigate(-1);
|
||||
opened = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
let opened = false;
|
||||
const handleClick = () => {
|
||||
opened = true;
|
||||
navigate(`/exercices/${exo.id_code}`);
|
||||
show(
|
||||
ModalCard,
|
||||
{
|
||||
exo,
|
||||
exos: exerciceStore,
|
||||
tags: tagsStore
|
||||
},
|
||||
() => {
|
||||
navigate(-1);
|
||||
opened = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="card" on:click={handleClick} on:dblclick={() => {}} on:keypress={() => {}}>
|
||||
<h1>{exo.name}</h1>
|
||||
<div class="examples">
|
||||
{#if exo.examples != null}
|
||||
<h2>Exemples</h2>
|
||||
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
|
||||
{#each exo.examples.data.slice(0, 3) as ex}
|
||||
<p>{ex.calcul}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Aucun exemple disponible</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !!$isAuth && exo.is_author && exo.original == null }
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{:else if !exo.is_author}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={'blue'}>
|
||||
Par <strong>{exo.author.username}</strong>
|
||||
</PrivacyIndicator>
|
||||
{#if !!$isAuth}
|
||||
<div
|
||||
data-testid="copy"
|
||||
class="icon"
|
||||
on:keydown={() => {}}
|
||||
on:click|stopPropagation={() => {
|
||||
<h1>{exo.name}</h1>
|
||||
<div class="examples">
|
||||
{#if exo.examples != null}
|
||||
<h2>Exemples</h2>
|
||||
{#if !!exo.consigne}<p data-testid="consigne">{exo.consigne}</p>{/if}
|
||||
{#each exo.examples.data.slice(0, 3) as ex}
|
||||
<p>{ex.calcul}</p>
|
||||
{/each}
|
||||
{:else}
|
||||
<p>Aucun exemple disponible</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !!$isAuth && exo.is_author && exo.original == null }
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{:else if !exo.is_author}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={'blue'}>
|
||||
Par <strong>{exo.author.username}</strong>
|
||||
</PrivacyIndicator>
|
||||
{#if !!$isAuth}
|
||||
<div
|
||||
data-testid="copy"
|
||||
class="icon"
|
||||
on:keydown={() => {}}
|
||||
on:click|stopPropagation={() => {
|
||||
cloneExo(exo.id_code).then((r) => {
|
||||
goto('/exercices/' + r.id_code);
|
||||
show(ModalCard, { exo: r }, () => {
|
||||
@ -73,121 +73,133 @@
|
||||
});
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MdContentCopy />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if exo.is_author && exo.original != null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-hover" />
|
||||
{#if !!$isAuth}
|
||||
<TagContainer bind:exo />
|
||||
{/if}
|
||||
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||
>
|
||||
<MdContentCopy />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if exo.is_author && exo.original != null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-hover" />
|
||||
{#if !!$isAuth}
|
||||
<TagContainer bind:exo />
|
||||
{/if}
|
||||
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@import '../../variables';
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
* {
|
||||
transition: 0.45s;
|
||||
}
|
||||
|
||||
.examples {
|
||||
color: gray;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 20px;
|
||||
p {
|
||||
margin: 10px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transform: scale(0.9);
|
||||
|
||||
.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);
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 88%;
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
.examples {
|
||||
color: gray;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card {
|
||||
border: 1px solid black;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: $background;
|
||||
min-height: 250px;
|
||||
max-height: 300px;
|
||||
&:hover {
|
||||
transform: translateX(10px) translateY(-10px);
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 10px;
|
||||
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 {
|
||||
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>
|
||||
|
@ -1,33 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { Exercice, Page } from '../../types/exo.type';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import EditForm from './EditForm.svelte';
|
||||
import type { Exercice, Page } from "../../types/exo.type";
|
||||
import type { Writable } from "svelte/store";
|
||||
import EditForm from "./EditForm.svelte";
|
||||
|
||||
export let cancel: Function;
|
||||
export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>;
|
||||
const updateExo = (e: Exercice) => {
|
||||
exos.update((o) => {
|
||||
return { ...o, data: { ...o.data, items: [e, ...o.data.items] } };
|
||||
});
|
||||
};
|
||||
export let cancel: Function;
|
||||
export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>;
|
||||
const updateExo = (e: Exercice) => {
|
||||
exos.update((o) => {
|
||||
return { ...o, data: { ...o.data, items: [e, ...o.data.items] } };
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h1>Nouvel exercice</h1>
|
||||
<EditForm editing={false} {cancel} {updateExo} />
|
||||
<h1>Nouvel exercice</h1>
|
||||
<EditForm editing={false} {cancel} {updateExo} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
div {
|
||||
background: $background;
|
||||
padding: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
@import '../../variables';
|
||||
@import '../../mixins';
|
||||
|
||||
div {
|
||||
background: $background;
|
||||
padding: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
@include down(800){
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
min-width: 800px;
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
@ -86,7 +86,7 @@
|
||||
{/if}
|
||||
<div class="icons">
|
||||
<div>
|
||||
<div class="icon" style:color="black" on:click={() => close()} on:keypress={() => {}}>
|
||||
<div class="icon contrast" on:click={() => close()} on:keypress={() => {}}>
|
||||
<MdClose />
|
||||
</div>
|
||||
</div>
|
||||
@ -148,6 +148,7 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../mixins';
|
||||
.icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
@ -183,7 +184,7 @@
|
||||
span.name {
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
span:not(.name) {
|
||||
position: relative;
|
||||
@ -191,6 +192,11 @@
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@include down(750px){
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.examples {
|
||||
|
@ -1,43 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { form, field } from 'svelte-forms';
|
||||
import { required, max, min } from 'svelte-forms/validators';
|
||||
import FileInput from '../forms/FileInput.svelte';
|
||||
import InputWithLabel from '../forms/InputWithLabel.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { createExo, editExo } from '../../requests/exo.request';
|
||||
import type { Exercice } from '../../types/exo.type';
|
||||
import { checkFile, errorMsg } from '../../utils/forms';
|
||||
import { compareObject } from '../../utils/utils';
|
||||
|
||||
export let editing = true;
|
||||
export let updateExo: Function = (e: Exercice) => {};
|
||||
import { form, field } from "svelte-forms";
|
||||
import { required, max, min } from "svelte-forms/validators";
|
||||
import FileInput from "../forms/FileInput.svelte";
|
||||
import InputWithLabel from "../forms/InputWithLabel.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { createExo, editExo } from "../../requests/exo.request";
|
||||
import type { Exercice } from "../../types/exo.type";
|
||||
import { checkFile, errorMsg } from "../../utils/forms";
|
||||
import { compareObject } from "../../utils/utils";
|
||||
import { goto } from "$app/navigation";
|
||||
import ModalCard from "./ModalCard.svelte";
|
||||
|
||||
export let exo: Exercice | null = null;
|
||||
export let cancel: Function;
|
||||
export let editing = true;
|
||||
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)
|
||||
/* let list = new DataTransfer();
|
||||
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
|
||||
list.items.add(file);
|
||||
!editing && list.items.remove(0); */
|
||||
const { alert } = getContext<{ alert: Function }>("alert");
|
||||
const { show } = getContext<{ show: Function }>("modal");
|
||||
|
||||
// Initiate fields and form
|
||||
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
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);
|
||||
// "Legally" initiate empty FileList for model field (simple list raises warning)
|
||||
/* let list = new DataTransfer();
|
||||
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
|
||||
const name = field("name", !!exo ? exo.name : "", [required(), max(50), min(5)], {
|
||||
checkOnInit: true
|
||||
});
|
||||
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>
|
||||
|
||||
<form
|
||||
action=""
|
||||
on:submit|preventDefault={() => {
|
||||
action=""
|
||||
on:submit|preventDefault={() => {
|
||||
loading = true
|
||||
if (editing && exo != null) {
|
||||
editExo(exo.id_code, {
|
||||
name: $name.value,
|
||||
@ -45,54 +55,85 @@
|
||||
private: $prv.value,
|
||||
...($model.dirty == true && { file: $model.value[0] })
|
||||
}).then((r) => {
|
||||
loading=false
|
||||
success('Exercice modifié !', `Exercice ${r.data.name} modifié avec succès !`)
|
||||
exo=r.data
|
||||
updateExo(r.data);
|
||||
cancel()
|
||||
});
|
||||
}).catch((e) => {
|
||||
loading=false
|
||||
console.log(e)
|
||||
error('Erreur', 'Une erreur est survenue lors de la modification de l\'exercice')
|
||||
});
|
||||
} else {
|
||||
|
||||
createExo({
|
||||
name: $name.value,
|
||||
consigne: $consigne.value,
|
||||
private: $prv.value,
|
||||
file: $model.value[0]
|
||||
}).then((r) => {
|
||||
loading=false
|
||||
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
|
||||
type="text"
|
||||
bind:value={$name.value}
|
||||
maxlength="50"
|
||||
minlength="5"
|
||||
required
|
||||
label="Nom"
|
||||
errors={errorMsg($myForm, 'name')}
|
||||
name="name"
|
||||
/>
|
||||
<InputWithLabel
|
||||
type="text"
|
||||
bind:value={$name.value}
|
||||
maxlength="50"
|
||||
minlength="5"
|
||||
required
|
||||
label="Nom"
|
||||
errors={errorMsg($myForm, 'name')}
|
||||
name="name"
|
||||
/>
|
||||
|
||||
<InputWithLabel
|
||||
type="text"
|
||||
bind:value={$consigne.value}
|
||||
maxlength="200"
|
||||
label="Consigne"
|
||||
errors={errorMsg($myForm, 'consigne')}
|
||||
name="consigne"
|
||||
/>
|
||||
<InputWithLabel
|
||||
type="text"
|
||||
bind:value={$consigne.value}
|
||||
maxlength="200"
|
||||
label="Consigne"
|
||||
errors={errorMsg($myForm, 'consigne')}
|
||||
name="consigne"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
||||
<label for="private">Privé</label>
|
||||
</div>
|
||||
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} defaultValue={editing &&exo!= null? exo.exo_source: null}/>
|
||||
<div>
|
||||
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
||||
<label for="private">Privé</label>
|
||||
</div>
|
||||
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code}
|
||||
defaultValue={editing &&exo!= null? exo.exo_source: null} />
|
||||
|
||||
<div class="wp-100">
|
||||
<button class="primary-btn" disabled={!$myForm.valid}>Modifier</button>
|
||||
<button
|
||||
class="danger-btn"
|
||||
on:click|preventDefault={() => {
|
||||
<div class="wp-100">
|
||||
<button class="primary-btn" disabled={!$myForm.valid}>
|
||||
{#if !loading}
|
||||
{editing ? "Modifier" : "Créer"}
|
||||
{: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()))) {
|
||||
alert({
|
||||
@ -104,17 +145,23 @@
|
||||
} else {
|
||||
cancel();
|
||||
}
|
||||
}}>Annuler</button
|
||||
>
|
||||
</div>
|
||||
}}>Annuler
|
||||
</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.spinner{
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-width: 2px!important;
|
||||
}
|
||||
</style>
|
||||
|
@ -137,6 +137,7 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../mixins';
|
||||
.auth-head {
|
||||
display: flex;
|
||||
|
||||
@ -157,6 +158,11 @@
|
||||
background-color: $background;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
@include down(800){
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
|
@ -112,54 +112,59 @@
|
||||
|
||||
</script>
|
||||
|
||||
{#if $tagStore.data != undefined}
|
||||
<Head location={filter} bind:search bind:selected />
|
||||
{/if}
|
||||
{#if $tagStore.isFetching == true}
|
||||
Fetching
|
||||
{/if}
|
||||
|
||||
<div class="feed">
|
||||
<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>
|
||||
<div class="full" class:loading={$tagStore.isFetching || $exerciceStore.isFetching}>
|
||||
|
||||
|
||||
{#if $tagStore.data != undefined}
|
||||
<Head location={filter} bind:search bind:selected />
|
||||
{/if}
|
||||
|
||||
|
||||
<div class="feed">
|
||||
<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}
|
||||
<h1>
|
||||
Tous les <span>exercices</span>
|
||||
</h1>
|
||||
<p>Vous retrouverez ici tous les exercices créés par les autres utilisateurs</p>
|
||||
{#each Array(10) as i}
|
||||
<div class="skeleton"><span /></div>
|
||||
{/each}
|
||||
{/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>Aucun exercices</p>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each Array(10) as i}
|
||||
<div class="skeleton"><span /></div>
|
||||
{/each}
|
||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $exerciceStore.data != undefined}
|
||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
@import "../../mixins.scss";
|
||||
|
||||
.skeleton {
|
||||
width: 330px;
|
||||
//width: 330px;
|
||||
max-width: 330px;
|
||||
height: 250px;
|
||||
opacity: .8;
|
||||
|
||||
@ -214,6 +219,10 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.full {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-column: 1/3;
|
||||
display: flex;
|
||||
@ -221,6 +230,11 @@
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
@include down(600) {
|
||||
grid-column: 1/2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.5em;
|
||||
font-weight: bolder;
|
||||
@ -235,4 +249,11 @@
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
.empty{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -70,6 +70,7 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../mixins.scss";
|
||||
.head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -77,6 +78,17 @@
|
||||
div {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@include down(600){
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
> * {
|
||||
width: 100%!important;
|
||||
}
|
||||
button{
|
||||
width: 100%!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
|
@ -51,6 +51,7 @@
|
||||
}}
|
||||
/>
|
||||
{:else if editing === true}
|
||||
<h1>Modification</h1>
|
||||
<EditForm
|
||||
bind:exo
|
||||
cancel={() => {
|
||||
@ -63,8 +64,10 @@
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
@import '../../mixins';
|
||||
|
||||
.modal {
|
||||
min-width: 820px;
|
||||
min-width: 800px;
|
||||
background: $background;
|
||||
padding: 70px;
|
||||
grid-gap: 10px;
|
||||
@ -73,5 +76,15 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
@include down(800){
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
h1{
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -104,7 +104,6 @@
|
||||
display: flex;
|
||||
margin: 30px;
|
||||
height: max-content;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
button {
|
||||
|
@ -27,7 +27,7 @@
|
||||
</script>
|
||||
|
||||
<span class="inputLabel" class:error={errors.length !== 0}>
|
||||
|
||||
<div style:position="relative">
|
||||
<input
|
||||
use:typeAction
|
||||
on:input={(e)=>{change(e)}}
|
||||
@ -48,7 +48,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<span class="bar" />
|
||||
|
||||
{#if errors.length !== 0}
|
||||
|
@ -1,91 +1,91 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
Challenge,
|
||||
Member,
|
||||
ParcoursInfos,
|
||||
Room,
|
||||
Note as NoteType
|
||||
} from '../../types/room.type';
|
||||
import { getContext, onDestroy, onMount } from "svelte";
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { challenge, corrigeChallenge, getChallenge, getParcours, sendChallenge } from '../../requests/room.request';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import InputChallenge from './InputChallenge.svelte';
|
||||
import { parseTimer } from '../../utils/utils';
|
||||
import FaUndo from 'svelte-icons/fa/FaUndo.svelte';
|
||||
import type {
|
||||
Challenge,
|
||||
Member,
|
||||
ParcoursInfos,
|
||||
Room,
|
||||
Note as NoteType
|
||||
} from "../../types/room.type";
|
||||
import { getContext, onDestroy, onMount } from "svelte";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { challenge, corrigeChallenge, getChallenge, getParcours, sendChallenge } from "../../requests/room.request";
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/stores";
|
||||
import InputChallenge from "./InputChallenge.svelte";
|
||||
import { parseTimer } from "../../utils/utils";
|
||||
import FaUndo from "svelte-icons/fa/FaUndo.svelte";
|
||||
|
||||
const room: Writable<Room> = getContext('room');
|
||||
const member: Writable<Member> = getContext('member');
|
||||
const room: Writable<Room> = getContext("room");
|
||||
const member: Writable<Member> = getContext("member");
|
||||
|
||||
const challengeStore: Writable<{
|
||||
challenge: Challenge[];
|
||||
id_code: string;
|
||||
parcours: ParcoursInfos;
|
||||
corriged: boolean;
|
||||
mistakes?: number
|
||||
validated?: boolean;
|
||||
challenger?: { name: string };
|
||||
isCorriged?: boolean,
|
||||
} | null> = writable(null);
|
||||
const challengeStore: Writable<{
|
||||
challenge: Challenge[];
|
||||
id_code: string;
|
||||
parcours: ParcoursInfos;
|
||||
corriged: boolean;
|
||||
mistakes?: number
|
||||
validated?: boolean;
|
||||
challenger?: { name: string };
|
||||
isCorriged?: boolean,
|
||||
} | null> = writable(null);
|
||||
|
||||
export let id_code: string;
|
||||
export let corrige: boolean = false;
|
||||
export let id_code: string;
|
||||
export let corrige: boolean = false;
|
||||
|
||||
onMount(()=>{
|
||||
if(!corrige) {
|
||||
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
|
||||
challengeStore.set({ ...p, corriged: false });
|
||||
});
|
||||
} else {
|
||||
onMount(() => {
|
||||
if (!corrige) {
|
||||
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
|
||||
challengeStore.set({ ...p, corriged: false });
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!corrige && $challengeStore != null && remaining == null) {
|
||||
remaining = $challengeStore.parcours.time * 60;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!corrige && $challengeStore != null && timer == null && remaining != null) {
|
||||
timer = window.setInterval(() => {
|
||||
remaining = remaining! - 1;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (timer != null) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (timer != null) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $challengeStore != null}
|
||||
<div class="head">
|
||||
<h1>
|
||||
{$challengeStore.parcours.name}
|
||||
<div class="full">
|
||||
{#if $challengeStore != null}
|
||||
<div class="head">
|
||||
<h1>
|
||||
{$challengeStore.parcours.name}
|
||||
|
||||
{#if corrige && !!$challengeStore.challenger && remaining != null}
|
||||
{#if corrige && !!$challengeStore.challenger && remaining != null}
|
||||
<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>
|
||||
en {parseTimer(remaining)} (cliquez sur les réponses pour voir)</span
|
||||
>
|
||||
{:else}
|
||||
>
|
||||
{:else}
|
||||
<span
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
class="icon"
|
||||
on:click={() => {
|
||||
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then(
|
||||
(p) => {
|
||||
challengeStore.set({ ...p, corriged: false });
|
||||
@ -97,63 +97,67 @@
|
||||
}
|
||||
);
|
||||
}}
|
||||
title={'Réessayer'}
|
||||
on:keydown={() => {}}><FaUndo /></span
|
||||
>
|
||||
{/if}
|
||||
</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>
|
||||
title={'Réessayer'}
|
||||
on:keydown={() => {}}><FaUndo /></span
|
||||
>
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
{#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>
|
||||
<p>
|
||||
{e.exo.consigne}
|
||||
</p>
|
||||
</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(']', ''))]
|
||||
{#if $challengeStore.mistakes}
|
||||
<p class="mistakes" class:validated={$challengeStore.validated}>{$challengeStore.mistakes} fautes</p>
|
||||
{/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}`)}
|
||||
<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}
|
||||
corriged={$challengeStore.corriged}
|
||||
bind:valid={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].valid}
|
||||
corrigeable={corrige}
|
||||
admin = {$member.isAdmin}
|
||||
/>
|
||||
{:else}
|
||||
{i}{' '}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
{#if !corrige}
|
||||
<button
|
||||
hidden={$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
corriged={$challengeStore.corriged}
|
||||
bind:valid={c.inputs[parseInt(i.replace('[', '').replace(']', ''))].valid}
|
||||
corrigeable={corrige}
|
||||
admin={$member.isAdmin}
|
||||
/>
|
||||
{:else}
|
||||
{i}{' '}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div>
|
||||
{#if !corrige}
|
||||
<button
|
||||
hidden={$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
if ($challengeStore == null || remaining == null) return;
|
||||
sendChallenge(
|
||||
$room.id_code,
|
||||
@ -175,12 +179,13 @@
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
}}>Valider !</button
|
||||
>
|
||||
<button
|
||||
hidden={!$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
}}>Valider !
|
||||
</button
|
||||
>
|
||||
<button
|
||||
hidden={!$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
console.log('RETRY CLICKED')
|
||||
challenge($room.id_code, id_code, !$member.isUser ? $member.clientId : null).then((p) => {
|
||||
challengeStore.set({ ...p, corriged: false });
|
||||
@ -190,107 +195,149 @@
|
||||
timer = null;
|
||||
}
|
||||
});
|
||||
}}>Réessayer !</button
|
||||
>
|
||||
{:else if $member.isAdmin}
|
||||
<button
|
||||
hidden={!$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
}}>Réessayer !
|
||||
</button
|
||||
>
|
||||
{:else if $member.isAdmin}
|
||||
<button
|
||||
hidden={!$challengeStore.corriged}
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
corrigeChallenge($room.id_code, id_code,$challengeStore?.challenge, !$member.isUser ? $member.clientId : null).then((p) => {
|
||||
if($challengeStore == null) return
|
||||
$challengeStore.challenge = p.data
|
||||
$challengeStore.mistakes = p.mistakes
|
||||
$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
|
||||
>
|
||||
{/if}
|
||||
}}>Valider !
|
||||
</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="danger-btn"
|
||||
on:click={() => {
|
||||
<button
|
||||
class="danger-btn"
|
||||
on:click={() => {
|
||||
if ($challengeStore == null) return;
|
||||
goto(`?${new URLSearchParams({p: $challengeStore.parcours.id_code}).toString()}`);
|
||||
}}>{!$challengeStore.corriged?"Annuler !":"Retour"}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
}}>{!$challengeStore.corriged ? "Annuler !" : "Retour"}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<style lang="scss">
|
||||
.timer {
|
||||
font-size: 2em;
|
||||
color: $green;
|
||||
font-weight: 800;
|
||||
}
|
||||
@import "../../mixins";
|
||||
|
||||
.oneminute {
|
||||
color: $orange;
|
||||
}
|
||||
.full {
|
||||
padding: 7px 15px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 40px 0;
|
||||
min-height: 70px;
|
||||
}
|
||||
.timer {
|
||||
font-size: 2em;
|
||||
color: $green;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.late {
|
||||
color: $red;
|
||||
}
|
||||
.oneminute {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
.calcul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.data {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 40px 0;
|
||||
min-height: 70px;
|
||||
@include down(800px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
h1{
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.infos {
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
span {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
text-decoration: underline;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@include up(800px) {
|
||||
.correction-info::before{
|
||||
content: " - ";
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.late {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.calcul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.infos {
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
|
||||
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>
|
||||
|
@ -1,113 +1,134 @@
|
||||
<script lang="ts">
|
||||
import type { Member, ParcoursRead } from '../../types/room.type';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import IoIosArrowDown from 'svelte-icons/io/IoIosArrowDown.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { parseTimer } from '../../utils/utils';
|
||||
import IoMdOpen from 'svelte-icons/io/IoMdOpen.svelte';
|
||||
import type { Member, ParcoursRead } from "../../types/room.type";
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import IoIosArrowDown from "svelte-icons/io/IoIosArrowDown.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { parseTimer } from "../../utils/utils";
|
||||
import IoMdOpen from "svelte-icons/io/IoMdOpen.svelte";
|
||||
|
||||
const parcours: Writable<ParcoursRead | null> = getContext('parcours');
|
||||
const member: Writable<Member | null> = getContext('member');
|
||||
const parcours: Writable<ParcoursRead | null> = getContext("parcours");
|
||||
const member: Writable<Member | null> = getContext("member");
|
||||
|
||||
let selected = "";
|
||||
|
||||
let selected = '';
|
||||
</script>
|
||||
|
||||
{#if $parcours != null && $member != null}
|
||||
<div class="trylist">
|
||||
{#if Object.keys($parcours.challenges).length == 0}
|
||||
<p class="italic">Aucun essai effectué :(</p>
|
||||
{/if}
|
||||
<div class="trylist">
|
||||
{#if Object.keys($parcours.challenges).length == 0}
|
||||
<p class="italic">Aucun essai effectué :(</p>
|
||||
{/if}
|
||||
|
||||
{#each Object.entries($parcours.challenges) as [id, chall]}
|
||||
<p
|
||||
on:click={() => {
|
||||
{#each Object.entries($parcours.challenges) as [id, chall]}
|
||||
<p
|
||||
on:click={() => {
|
||||
selected = selected == chall.challenger.id_code ? '' : chall.challenger.id_code;
|
||||
}}
|
||||
class:selected={selected == chall.challenger.id_code}
|
||||
class="tries"
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<span class="icon"><IoIosArrowDown /></span>
|
||||
{chall.challenger.id_code == $member.id_code ? 'Vos essais' : chall.challenger.name}
|
||||
</p>
|
||||
class:selected={selected == chall.challenger.id_code}
|
||||
class="tries"
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<span class="icon"><IoIosArrowDown /></span>
|
||||
{chall.challenger.id_code == $member.id_code ? 'Vos essais' : chall.challenger.name}
|
||||
<span class:valid = {chall.challenger.validated} class="validation-status">{chall.challenger.validated ? "Validé": "Non validé"}</span>
|
||||
</p>
|
||||
|
||||
{#if selected == chall.challenger.id_code}
|
||||
{#each chall.challenges as c}
|
||||
<div
|
||||
class="try"
|
||||
on:click={() => {
|
||||
{#if selected == chall.challenger.id_code}
|
||||
{#each chall.challenges as c}
|
||||
<div
|
||||
class="try"
|
||||
on:click={() => {
|
||||
goto(`?${new URLSearchParams({corr: c.id_code}).toString()}`);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
title="Voir la correction"
|
||||
>
|
||||
<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>
|
||||
</p>
|
||||
<span class="corrige-link icon"><IoMdOpen /></span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
on:keydown={() => {}}
|
||||
title="Voir la correction"
|
||||
>
|
||||
<p><span class:validated={c.validated} class:uncorriged={!c.isCorriged} class="note"
|
||||
>{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>
|
||||
<span class="corrige-link icon"><IoMdOpen /></span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.tries {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
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);
|
||||
}
|
||||
}
|
||||
.tries {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
transition: 0.3s;
|
||||
|
||||
.try {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
width: max-content;
|
||||
margin-left: 30px;
|
||||
p span{
|
||||
color: $red;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform: rotate(-90deg);
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
}
|
||||
.selected {
|
||||
font-weight: 700;
|
||||
|
||||
.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>
|
||||
|
@ -4,11 +4,15 @@
|
||||
import FaLock from "svelte-icons/fa/FaLock.svelte";
|
||||
import { getContext } from "svelte";
|
||||
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 member: Writable<Member> = getContext("member");
|
||||
const { send } = getContext<{ send: Function }>("ws");
|
||||
|
||||
const { alert } = getContext<{ alert: Function }>("alert");
|
||||
$: online =
|
||||
$room != null
|
||||
? $room.members.filter((r): r is Member => "online" in r && r.online == true)
|
||||
@ -21,6 +25,16 @@
|
||||
$room != null
|
||||
? $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>
|
||||
|
||||
<div class="members">
|
||||
@ -62,9 +76,7 @@
|
||||
class:member={m.id_code == $member.id_code}
|
||||
class="online"
|
||||
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
|
||||
on:click={() => {
|
||||
$member.isAdmin && !m.isAdmin && send('ban', { member_id: m.id_code });
|
||||
}}
|
||||
on:click={(e)=>ban(m)}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{m.username}
|
||||
@ -86,9 +98,7 @@
|
||||
class:bannable={m.id_code != $member.id_code && $member.isAdmin && !m.isAdmin}
|
||||
class:member={m.id_code == $member.id_code}
|
||||
title={$member.isAdmin && !m.isAdmin ? 'Bannir' : ''}
|
||||
on:click={() => {
|
||||
$member.isAdmin && !m.isAdmin && send('ban', { member_id: m.id_code });
|
||||
}}
|
||||
on:click={(e)=>ban(m)}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
{m.username}
|
||||
@ -111,14 +121,20 @@
|
||||
class="accept"
|
||||
on:click={() => {
|
||||
send('accept', { waiter_id: m.waiter_id });
|
||||
}}>Accept
|
||||
}}
|
||||
title="Accepter"
|
||||
>
|
||||
<MdCheck />
|
||||
</button
|
||||
>
|
||||
<button
|
||||
class="refuse"
|
||||
on:click={() => {
|
||||
send('refuse', { waiter_id: m.waiter_id });
|
||||
}}>Refuse
|
||||
}}
|
||||
title="Refuser"
|
||||
>
|
||||
<MdClose />
|
||||
</button
|
||||
>
|
||||
</p>
|
||||
@ -171,6 +187,9 @@
|
||||
font-size: 1em;
|
||||
width: max-content;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
color: grey;
|
||||
font-size: 0.9em;
|
||||
@ -203,4 +222,27 @@
|
||||
font-weight: 500;
|
||||
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>
|
||||
|
@ -13,6 +13,8 @@
|
||||
import { messages, handlers } from "../../store/ws";
|
||||
import Stats from "./Stats.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;
|
||||
|
||||
@ -20,7 +22,7 @@
|
||||
const member: Writable<Member> = getContext("member");
|
||||
|
||||
const { send } = getContext<{ send: Function }>("ws");
|
||||
|
||||
const {alert} = getContext<{alert: Function}>("alert");
|
||||
const parcours: Writable<ParcoursRead | null> = getContext("parcours");
|
||||
|
||||
let open = "";
|
||||
@ -53,6 +55,27 @@
|
||||
>
|
||||
<FaEdit />
|
||||
</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}
|
||||
|
||||
<div
|
||||
@ -187,6 +210,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
||||
@ -235,6 +259,7 @@
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@ -269,4 +294,7 @@
|
||||
.edit {
|
||||
color: $green;
|
||||
}
|
||||
h1{
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,128 +1,150 @@
|
||||
<script lang="ts">
|
||||
import { delParcours } from '../../requests/room.request';
|
||||
import { getContext } from 'svelte';
|
||||
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Member, Room } from '../../types/room.type';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
const room: Writable<Room> = getContext('room');
|
||||
const member: Writable<Member> = getContext('member');
|
||||
const { alert } = getContext<{ alert: Function }>('alert');
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { Member, Room } from "../../types/room.type";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const room: Writable<Room> = getContext("room");
|
||||
const member: Writable<Member> = getContext("member");
|
||||
</script>
|
||||
|
||||
<div class="parcours">
|
||||
<div class="head">
|
||||
<h2>Parcours</h2>
|
||||
{#if $member.isAdmin}
|
||||
<button
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
<div class="head">
|
||||
<h2>Parcours</h2>
|
||||
{#if $member.isAdmin}
|
||||
<button
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
goto(`?${new URLSearchParams({ p: 'new' }).toString()}`);
|
||||
}}>Nouveau</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
}}>Nouveau
|
||||
</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
{#if $room.parcours.length == 0}
|
||||
<p class="empty">Aucun parcours pour le moment</p>
|
||||
{/if}
|
||||
{#each $room.parcours as p}
|
||||
<div
|
||||
on:click={() => {
|
||||
<div class="list">
|
||||
{#if $room.parcours.length == 0}
|
||||
<p class="empty">Aucun parcours pour le moment</p>
|
||||
{/if}
|
||||
{#each $room.parcours as p}
|
||||
<div
|
||||
on:click={() => {
|
||||
goto(`?${new URLSearchParams({ p: p.id_code }).toString()}`);
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<p>{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}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<p class="parcours-name">{p.name}</p>
|
||||
|
||||
{#if $member.isAdmin}
|
||||
<div
|
||||
class="icon delete"
|
||||
on:keydown={() => {}}
|
||||
on:click|stopPropagation={() => {
|
||||
alert({
|
||||
title: 'Supprimer ?',
|
||||
description: 'Voulez vous supprimer ce parcours ?',
|
||||
validate: () => {
|
||||
delParcours(
|
||||
$room?.id_code,
|
||||
p.id_code,
|
||||
!$member.isUser ? $member?.clientId : null
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaRegTrashAlt />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<p class="stat">
|
||||
{#if p.best_note}
|
||||
<strong>Record :</strong> {p.best_note} faute{p.best_note > 1 ? "s" : ""}
|
||||
{:else}
|
||||
Aucun essai effectué
|
||||
{/if}
|
||||
</p>
|
||||
<p class="stat valid" data-testid="valid"
|
||||
class:validated={p.validated}>{ p.validated ? "Parcours validé" : "Parcours non validé"}</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.empty {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.parcours {
|
||||
background-color: rgba($background, 0.4);
|
||||
border: 1px solid $border;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $border;
|
||||
padding: 10px 0;
|
||||
button {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $red;
|
||||
transition: 0.4s;
|
||||
opacity: 0;
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
.list {
|
||||
overflow: auto;
|
||||
> div {
|
||||
padding: 30px 10px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.parcours {
|
||||
background-color: rgba($background, 0.4);
|
||||
border: 1px solid $border;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $border;
|
||||
padding: 10px 0;
|
||||
|
||||
button {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: $red;
|
||||
transition: 0.4s;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow: auto;
|
||||
|
||||
> div {
|
||||
padding: 30px 10px;
|
||||
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>
|
||||
|
@ -1,47 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import FaRegTrashAlt from 'svelte-icons/fa/FaRegTrashAlt.svelte';
|
||||
import { getContext } from "svelte";
|
||||
import FaRegTrashAlt from "svelte-icons/fa/FaRegTrashAlt.svelte";
|
||||
|
||||
import FaUndo from 'svelte-icons/fa/FaUndo.svelte';
|
||||
import FaTimes from 'svelte-icons/fa/FaTimes.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Room, Member } from '../../types/room.type';
|
||||
import type ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
const room: Writable<Room> = getContext('room');
|
||||
const member: Writable<Member>= getContext('member');
|
||||
const { send, ws } = getContext<{send: Function, ws: ReconnectingWebSocket}>('ws');
|
||||
let editing = false;
|
||||
let name = $room.name;
|
||||
let r: HTMLInputElement;
|
||||
import FaUndo from "svelte-icons/fa/FaUndo.svelte";
|
||||
import FaTimes from "svelte-icons/fa/FaTimes.svelte";
|
||||
import IoMdLogOut from 'svelte-icons/io/IoMdLogOut.svelte'
|
||||
import { goto } from "$app/navigation";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { Member, Room } from "../../types/room.type";
|
||||
import type ReconnectingWebSocket from "reconnecting-websocket";
|
||||
import { deleteRoom } from "../../requests/room.request.js";
|
||||
|
||||
const room: Writable<Room> = getContext("room");
|
||||
const member: Writable<Member> = getContext("member");
|
||||
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>
|
||||
|
||||
<div class="head">
|
||||
<div class="title">
|
||||
{#if !editing}
|
||||
<h1
|
||||
on:dblclick={() => {
|
||||
<div class="title">
|
||||
{#if !editing}
|
||||
<h1
|
||||
on:dblclick={() => {
|
||||
if(!$member.isAdmin) return
|
||||
editing = true;
|
||||
name = $room.name;
|
||||
r.focus();
|
||||
console.log("OPENED")
|
||||
}}
|
||||
>
|
||||
{$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span>
|
||||
</h1>
|
||||
{/if}
|
||||
>
|
||||
{$room.name}<span on:dblclick|stopPropagation>#{$room.id_code}</span>
|
||||
</h1>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
class:hide={!editing}
|
||||
on:focusout={() => {
|
||||
<input
|
||||
bind:this={r}
|
||||
bind:value={name}
|
||||
class="input"
|
||||
class:hide={!editing}
|
||||
on:focusout={() => {
|
||||
editing = false;
|
||||
}}
|
||||
bind:value={name}
|
||||
bind:this={r}
|
||||
on:keydown={(e) => {
|
||||
on:keydown={(e) => {
|
||||
if (e.key == 'Escape') {
|
||||
editing = false;
|
||||
} else if (e.key == 'Enter') {
|
||||
@ -49,102 +52,134 @@
|
||||
editing = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="icons">
|
||||
{#if $member.isAdmin}
|
||||
<div class="icon trash" data-testid="delete"><FaRegTrashAlt /></div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="icon refresh"
|
||||
on:click={() => {
|
||||
console.log(ws)
|
||||
<div class="icons">
|
||||
{#if $member.isAdmin}
|
||||
<div class="icon trash" data-testid="delete" on:click={()=>{
|
||||
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",
|
||||
validate: ()=>{
|
||||
deleteRoom($room.id_code, !$member.isUser ? $member.clientId: null)
|
||||
},
|
||||
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();
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
data-testid="refresh"
|
||||
>
|
||||
<FaUndo />
|
||||
</div>
|
||||
<div
|
||||
data-testid="leave"
|
||||
class="icon trash"
|
||||
on:click={() => {
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<FaUndo />
|
||||
</div>
|
||||
<div
|
||||
class="icon trash"
|
||||
data-testid="leave"
|
||||
on:click={() => {
|
||||
ws.close();
|
||||
goto('/room/join')
|
||||
goto('/room/join')
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<FaTimes />
|
||||
</div>
|
||||
</div>
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<FaTimes />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.hide {
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
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;
|
||||
}
|
||||
.hide {
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.trash {
|
||||
color: $red;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: inherit;
|
||||
|
||||
.refresh {
|
||||
color: $contrast;
|
||||
&:hover {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
span {
|
||||
font-size: 0.5em;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
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>
|
||||
|
@ -1,118 +1,137 @@
|
||||
<script lang="ts">
|
||||
import type { ParcoursRead, Member } from '../../types/room.type';
|
||||
import { parseTimer, statsCalculator } from '../../utils/utils';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import Classement from './Classement.svelte';
|
||||
import type { ParcoursRead, Member } from "../../types/room.type";
|
||||
import { parseTimer, statsCalculator } from "../../utils/utils";
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import Classement from "./Classement.svelte";
|
||||
|
||||
const parcours: Writable<ParcoursRead | null> = getContext('parcours');
|
||||
const member: Writable<Member | null> = getContext('member');
|
||||
const parcours: Writable<ParcoursRead | null> = getContext("parcours");
|
||||
const member: Writable<Member | null> = getContext("member");
|
||||
|
||||
$: stats =
|
||||
$parcours != null && $member != null && !!$parcours.challenges[$member.id_code]
|
||||
? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes))
|
||||
: null;
|
||||
$: stats =
|
||||
$parcours != null && $member != null && !!$parcours.challenges[$member.id_code]
|
||||
? statsCalculator($parcours.challenges[$member.id_code].challenges.map((c) => c.mistakes))
|
||||
: null;
|
||||
</script>
|
||||
|
||||
{#if $parcours != null}
|
||||
<div class="stats">
|
||||
<h1 class:validated={$parcours.validated}>
|
||||
{$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'}
|
||||
</h1>
|
||||
<div class="stats">
|
||||
<h1 class:validated={$parcours.validated}>
|
||||
{$parcours.validated ? 'Parcours validé !' : 'Parcours non validé !'}
|
||||
</h1>
|
||||
|
||||
<div class="statistics">
|
||||
<p data-testid="avg">
|
||||
<strong>Moyenne</strong>
|
||||
{stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined
|
||||
? `${stats?.avg.toFixed(2)} fautes`
|
||||
: '-'}
|
||||
</p>
|
||||
<p data-testid="max">
|
||||
<strong>Pire</strong>
|
||||
{stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'}
|
||||
</p>
|
||||
<p data-testid="min">
|
||||
<strong>Meilleur</strong>
|
||||
{stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="statistics">
|
||||
<p data-testid="avg">
|
||||
<strong>Moyenne</strong>
|
||||
{stats?.avg.toFixed(2) !== null && stats?.avg.toFixed(2) !== undefined
|
||||
? `${stats?.avg.toFixed(2)} fautes`
|
||||
: '-'}
|
||||
</p>
|
||||
<p data-testid="max">
|
||||
<strong>Pire</strong>
|
||||
{stats?.max !== null && stats?.max !== undefined ? `${stats?.max} fautes` : '-'}
|
||||
</p>
|
||||
<p data-testid="min">
|
||||
<strong>Meilleur</strong>
|
||||
{stats?.min !== null && stats?.min !== undefined ? `${stats?.min} fautes` : '-'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="classement">
|
||||
<h2>Classements</h2>
|
||||
<div class="ranks">
|
||||
<div>
|
||||
<h3>Top élèves</h3>
|
||||
<Classement
|
||||
tops={$parcours.ranking.map((t) => {
|
||||
<div class="classement">
|
||||
<h2>Classements</h2>
|
||||
<div class="ranks">
|
||||
<div>
|
||||
<h3>Top élèves</h3>
|
||||
<Classement
|
||||
tops={$parcours.ranking.map((t) => {
|
||||
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,
|
||||
rank: $parcours.memberRank,
|
||||
value: `Moyenne : ${stats?.avg.toFixed(2)} fautes`
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style:text-align="right">Top essais</h3>
|
||||
<Classement
|
||||
tops={$parcours.tops.map((t) => {
|
||||
<div>
|
||||
<h3 style:text-align="right">Top essais</h3>
|
||||
<Classement
|
||||
tops={$parcours.tops.map((t) => {
|
||||
return {
|
||||
name: t.challenger.name,
|
||||
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,
|
||||
rank: $parcours.rank,
|
||||
value: `${$parcours.pb.mistakes} fautes en ${parseTimer($parcours.pb.time)}`
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
.statistics {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
p {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@import "../../mixins.scss";
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
color: $red;
|
||||
text-align: center;
|
||||
}
|
||||
.validated {
|
||||
color: $green;
|
||||
}
|
||||
.ranks {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.classement {
|
||||
width: 100%;
|
||||
h2{
|
||||
text-align: center;
|
||||
}
|
||||
.statistics {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: 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>
|
||||
|
@ -94,7 +94,7 @@
|
||||
if (checkExpire(exp) && refresh != null) {
|
||||
refreshRequest(refresh).then((r) => {
|
||||
localStorage.setItem('token', r.access_token);
|
||||
$username = username;
|
||||
$username = name;
|
||||
$isAuth = true;
|
||||
});
|
||||
|
||||
|
@ -69,16 +69,19 @@
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
//width: 50%;
|
||||
transform: translateX(-50%) translateY(-50%) scale(0.7);
|
||||
visibility: hidden;
|
||||
transition: 0.4s;
|
||||
opacity: 0;
|
||||
z-index: 1000;
|
||||
height: 57vh;
|
||||
max-height: 57vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
|
||||
&.visible {
|
||||
visibility: visible !important;
|
||||
transform: translateX(-50%) translateY(-50%) scale(1) !important;
|
||||
@ -95,7 +98,7 @@
|
||||
.overlay {
|
||||
background-color: black;
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
z-index: 500;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -1,160 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
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;
|
||||
};
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import SvelteMarkdown from "svelte-markdown";
|
||||
|
||||
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 = () => {
|
||||
return Math.round(Date.now() * Math.random());
|
||||
};
|
||||
const notifications = writable<Notification[]>([]);
|
||||
|
||||
const toast = (notif: Notif) => {
|
||||
let id = getId();
|
||||
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 getId = () => {
|
||||
return Math.round(Date.now() * Math.random());
|
||||
};
|
||||
|
||||
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 });
|
||||
const toast = (notif: Notif) => {
|
||||
let id = getId();
|
||||
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) => {
|
||||
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>
|
||||
|
||||
<slot />
|
||||
|
||||
<div class="notifs" class:empty ={$notifications.length == 0}>
|
||||
{#each $notifications as n}
|
||||
<div
|
||||
on:click={() => {
|
||||
<div class="notifs" class:empty={$notifications.length == 0}>
|
||||
{#each $notifications as n}
|
||||
<div
|
||||
on:click={() => {
|
||||
n.deleted = true;
|
||||
setTimeout(() => {
|
||||
notifications.update((o) => o.filter((i) => i.id != n.id));
|
||||
}, 500);
|
||||
}}
|
||||
class={n.type}
|
||||
on:keydown={() => {}}
|
||||
class:deleted={n.deleted}
|
||||
>
|
||||
<h1>{n.title}</h1>
|
||||
<p><SvelteMarkdown source={n.description} /></p>
|
||||
</div>
|
||||
{/each}
|
||||
class={n.type}
|
||||
on:keydown={() => {}}
|
||||
class:deleted={n.deleted}
|
||||
>
|
||||
<h1>{n.title}</h1>
|
||||
<p>
|
||||
<SvelteMarkdown source={n.description} />
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.notifs {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding: 30px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
z-index: 1000;
|
||||
.notifs {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 30px;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(calc(100% - 60px), 500px);
|
||||
z-index: 500;
|
||||
|
||||
div {
|
||||
background-color: $background;
|
||||
z-index: 1000;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div {
|
||||
background-color: $background;
|
||||
z-index: 1000;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
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%;
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
z-index: 2;
|
||||
}
|
||||
&.deleted {
|
||||
transition: 0.5s;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty{
|
||||
z-index: -20;
|
||||
}
|
||||
.alert::after{
|
||||
background-color: $orange;
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
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;
|
||||
}
|
||||
|
||||
&.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{
|
||||
background-color: $red;
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
.success::after{
|
||||
background-color: $green;
|
||||
}
|
||||
@keyframes slide {
|
||||
from {
|
||||
width: 0%;
|
||||
}
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import axios from 'axios';
|
||||
import {authInstance} from '../apis/auth.api';
|
||||
import { env } from "$env/dynamic/public";
|
||||
|
||||
export const loginRequest = (data: { username: string; password: string }) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: 'http://localhost:8002/login',
|
||||
url: '/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: {
|
||||
@ -23,7 +24,7 @@ export const registerRequest = (data: {
|
||||
}) => {
|
||||
return authInstance
|
||||
.request({
|
||||
url: 'http://localhost:8002/register',
|
||||
url: '/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
headers: {
|
||||
@ -37,9 +38,9 @@ export const registerRequest = (data: {
|
||||
};
|
||||
|
||||
export const refreshRequest = (token: string) => {
|
||||
return authInstance
|
||||
return axios
|
||||
.request({
|
||||
url: 'http://localhost:8002/refresh',
|
||||
url: `${env.PUBLIC_API_BASE}/refresh`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
@ -131,9 +131,11 @@ export const getExoSource = (id_code: string,) => {
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
};export const generateRequest = (id_code: string,filename: string) => {
|
||||
};
|
||||
|
||||
export const generateRequest = (id_code: string,filename: string) => {
|
||||
return exoInstance({
|
||||
url: `/generator/csv/${id_code}/`,
|
||||
url: `generator/csv/${id_code}`,
|
||||
method: 'Get',
|
||||
params: {filename}
|
||||
}).then((r) => {
|
||||
|
@ -4,7 +4,7 @@ import { roomInstance } from '../apis/room.api';
|
||||
export const createRoom = (data: { name: string }, username: string | null = null) => {
|
||||
return roomInstance
|
||||
.request({
|
||||
url: '/',
|
||||
url: '',
|
||||
method: 'POST',
|
||||
params: { username },
|
||||
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) => {
|
||||
console.log('GETROOM', clientId, { ...(clientId != null && { clientId }) });
|
||||
return roomInstance
|
||||
.request({
|
||||
url: '/' + id_code,
|
||||
@ -23,6 +22,16 @@ export const getRoom = (id_code: string, clientId: string | null = null) => {
|
||||
.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 = (
|
||||
id_code: string,
|
||||
parcours: { time: number; name: string; max_mistakes: number; exercices: {exercice_id: string, quantity:number}[] },
|
||||
|
@ -21,6 +21,7 @@
|
||||
<Auth>
|
||||
<Alert>
|
||||
<Modal>
|
||||
|
||||
<main>
|
||||
<NavBar/>
|
||||
<slot/>
|
||||
@ -78,7 +79,7 @@
|
||||
height: calc(100vh - var(--navbar-height) - 10px);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
overflow-x: hidden;
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
|
@ -1,10 +1,30 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
|
||||
import {goto} from "$app/navigation";
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={() => {
|
||||
}}>test</button
|
||||
>
|
||||
<div class="rooms">
|
||||
<h1>Générateur d'exercices</h1>
|
||||
<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>
|
||||
|
||||
|
@ -1,42 +1,42 @@
|
||||
<script lang='ts'>
|
||||
import {getContext} from "svelte";
|
||||
import {dashBoardRequest} from "../../requests/auth.request";
|
||||
import Section from "../../components/auth/Section.svelte";
|
||||
import InfoForm from "../../components/auth/InfoForm.svelte";
|
||||
import RoomList from "../../components/auth/RoomList.svelte";
|
||||
import type {User} from "../../types/auth.type";
|
||||
import type {Writable} from "svelte/store";
|
||||
import {writable} from "svelte/store";
|
||||
import PasswordForm from "../../components/auth/PasswordForm.svelte";
|
||||
import {updatePassword, updateUserRequest} from "../../requests/auth.request.js";
|
||||
import UserConfirm from "../../components/auth/UserConfirm.svelte";
|
||||
import MdInfo from 'svelte-icons/md/MdInfo.svelte'
|
||||
import FaUsers from 'svelte-icons/fa/FaUsers.svelte'
|
||||
import FaUserLock from 'svelte-icons/fa/FaUserLock.svelte'
|
||||
import {goto} from "$app/navigation";
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { dashBoardRequest } from "../../requests/auth.request";
|
||||
import Section from "../../components/auth/Section.svelte";
|
||||
import InfoForm from "../../components/auth/InfoForm.svelte";
|
||||
import RoomList from "../../components/auth/RoomList.svelte";
|
||||
import type { User } from "../../types/auth.type";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
import PasswordForm from "../../components/auth/PasswordForm.svelte";
|
||||
import { updatePassword, updateUserRequest } from "../../requests/auth.request.js";
|
||||
import UserConfirm from "../../components/auth/UserConfirm.svelte";
|
||||
import MdInfo from "svelte-icons/md/MdInfo.svelte";
|
||||
import FaUsers from "svelte-icons/fa/FaUsers.svelte";
|
||||
import FaUserLock from "svelte-icons/fa/FaUserLock.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let u = '';
|
||||
let p = '';
|
||||
const user: Writable<User | null> = writable(null)
|
||||
const {logout, username, isAuth,initialLoading} = getContext('auth')
|
||||
let u = "";
|
||||
let p = "";
|
||||
const user: Writable<User | null> = writable(null);
|
||||
const { logout, username, isAuth, initialLoading } = getContext("auth");
|
||||
|
||||
$: !$initialLoading && $isAuth && dashBoardRequest().then((res) => {
|
||||
console.log(res)
|
||||
user.set(res)
|
||||
})
|
||||
const {show, close} = getContext("modal")
|
||||
const {success, error} = getContext("notif")
|
||||
let passwordForm = null
|
||||
let infoForm = null
|
||||
$: !$initialLoading && $isAuth && dashBoardRequest().then((res) => {
|
||||
console.log(res);
|
||||
user.set(res);
|
||||
});
|
||||
const { show, close } = getContext("modal");
|
||||
const { success, error } = getContext("notif");
|
||||
let passwordForm = null;
|
||||
let infoForm = null;
|
||||
|
||||
$: !$initialLoading && !$isAuth && goto('/')
|
||||
$: !$initialLoading && !$isAuth && goto("/");
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $user != null}
|
||||
<h1>Mon compte</h1>
|
||||
<Section title="Mes informations" icon={MdInfo} onValidate={()=>{
|
||||
{#if $user != null}
|
||||
<h1>Mon compte</h1>
|
||||
<Section title="Mes informations" icon={MdInfo} onValidate={()=>{
|
||||
if(infoForm == null) return
|
||||
updateUserRequest($infoForm.summary).then(res=>{
|
||||
user.update((o)=>{return {...o,...res}})
|
||||
@ -46,12 +46,12 @@
|
||||
})
|
||||
|
||||
}} canValid={infoForm != null && $infoForm.valid}>
|
||||
<InfoForm user={$user} bind:myForm={infoForm}/>
|
||||
</Section>
|
||||
<Section title="Salles" icon={FaUsers}>
|
||||
<RoomList rooms={$user.rooms}/>
|
||||
</Section>
|
||||
<Section title="Sécurité" icon={FaUserLock} validate="Modifier mon mot de passe" onValidate={()=>{
|
||||
<InfoForm user={$user} bind:myForm={infoForm} />
|
||||
</Section>
|
||||
<Section title="Salles" icon={FaUsers}>
|
||||
<RoomList rooms={$user.rooms} />
|
||||
</Section>
|
||||
<Section title="Sécurité" icon={FaUserLock} validate="Modifier mon mot de passe" onValidate={()=>{
|
||||
show(UserConfirm, {onValidate: ( p )=>{
|
||||
updatePassword({...$passwordForm.summary, old_password: p}).then((r)=>{
|
||||
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"})
|
||||
|
||||
}} canValid={passwordForm != null && $passwordForm.valid}>
|
||||
<PasswordForm bind:myForm={passwordForm}/>
|
||||
</Section>
|
||||
{/if}
|
||||
<PasswordForm bind:myForm={passwordForm} />
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<style lang='scss'>
|
||||
<style lang="scss">
|
||||
@import "../../mixins.scss";
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
@include down(800){
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 7px 15px;
|
||||
}
|
||||
</style>
|
@ -1,16 +1,31 @@
|
||||
<script lang="ts">
|
||||
|
||||
import {goto} from "$app/navigation";
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="rooms">
|
||||
<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/create')}}>Créer</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>
|
@ -26,13 +26,13 @@
|
||||
const room = writable<Room | null>(null);
|
||||
const member = writable<Member | 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("member", member);
|
||||
setContext("parcours", parcours);
|
||||
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 }) => {
|
||||
if (payload == undefined) return;
|
||||
const { type, data } = payload;
|
||||
@ -63,7 +63,6 @@
|
||||
if ($page.url.searchParams.get("a") == "waiting") {
|
||||
goto(`?`);
|
||||
}
|
||||
console.log("ACCEPTED", data.member);
|
||||
member.set(data.member);
|
||||
|
||||
getRoom($page.params.slug, !$isAuth ? data.member.clientId : null).then((r) => {
|
||||
@ -83,11 +82,13 @@
|
||||
|
||||
|
||||
case "waiting":
|
||||
close();
|
||||
$member = { ...data.waiter, room: data.room };
|
||||
goto(`?${new URLSearchParams({ a: "waiting" })}`);
|
||||
return;
|
||||
|
||||
case "refused":
|
||||
error("Refusé", "L'administrateur a refusé votre demande");
|
||||
close();
|
||||
ws?.close(1000);
|
||||
goto("/room/join");
|
||||
@ -101,17 +102,25 @@
|
||||
return;
|
||||
}
|
||||
if ($room != null) {
|
||||
info("Départ", `*${data.member.username}* n'est plus dans la salle`);
|
||||
$room.members = [
|
||||
...$room.members.filter((r) => "waiter_id" in r || r.id_code != data.member.id_code)
|
||||
];
|
||||
}
|
||||
return;
|
||||
case "banned":
|
||||
error("Ban", "Vous avez été banni de la salle par l'administrateur");
|
||||
ws?.close(1000);
|
||||
goto("/room/join");
|
||||
sessionStorage.removeItem("reconnect");
|
||||
return;
|
||||
|
||||
case "deleted":
|
||||
info("Suppression", "La salle a été supprimée par l'administrateur");
|
||||
ws.close(1000);
|
||||
goto("/room/join");
|
||||
return;
|
||||
|
||||
case "error":
|
||||
const { code, msg } = data;
|
||||
if (code == 401) {
|
||||
@ -148,6 +157,10 @@
|
||||
} else {
|
||||
error("Erreur", "Message : " + msg);
|
||||
}
|
||||
if (code == 404) {
|
||||
ws.close(1000);
|
||||
goto("/room/join");
|
||||
}
|
||||
return;
|
||||
case "waiter":
|
||||
|
||||
@ -199,6 +212,7 @@
|
||||
|
||||
case "joined":
|
||||
if ($room != null) {
|
||||
info("Arrivée", `*${data.member.username}* a rejoint la salle`)
|
||||
$room.members = [
|
||||
...$room?.members.filter(
|
||||
(m) =>
|
||||
@ -346,8 +360,9 @@
|
||||
$parcours.validated = $parcours.pb.mistakes <= $parcours.max_mistakes;
|
||||
}
|
||||
}
|
||||
return
|
||||
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[
|
||||
data.member
|
||||
].challenges.map((c) => {
|
||||
@ -356,8 +371,24 @@
|
||||
}
|
||||
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"));
|
||||
onDestroy(() => {
|
||||
|
||||
ws.close(1000);
|
||||
});
|
||||
|
||||
@ -507,6 +539,7 @@
|
||||
column-gap: 50px;
|
||||
row-gap: 10px;
|
||||
height: 100%;
|
||||
padding: 7px 15px;
|
||||
@include down(800) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -7,6 +7,8 @@
|
||||
let name = "";
|
||||
let pseudo = "";
|
||||
const { isAuth } = getContext("auth");
|
||||
let loading = false;
|
||||
const { error } = getContext("notif");
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
@ -20,15 +22,27 @@
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
console.log('(NAME)', name)
|
||||
loading = true
|
||||
createRoom({ name }, !$isAuth ? pseudo : null).then((r) => {
|
||||
|
||||
if(!$isAuth){
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,4 +65,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
|
||||
let room = "";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
@ -11,6 +11,7 @@
|
||||
$: {
|
||||
!$initialLoading && !!$isAuth && goto('/dashboard');
|
||||
}
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="parent">
|
||||
@ -37,13 +38,19 @@
|
||||
<button
|
||||
class="primary-btn"
|
||||
on:click={() => {
|
||||
login(username, password).catch((r) => {
|
||||
loading = true;
|
||||
login(username, password).then(()=>{loading=false}).catch((r) => {
|
||||
errors = { ...errors, ...r.data.detail };
|
||||
loading = false;
|
||||
});
|
||||
}}
|
||||
|
||||
>
|
||||
Se connecter
|
||||
{#if loading}
|
||||
<span class="spinner" />
|
||||
{:else}
|
||||
Se connecter
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,4 +72,9 @@
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
.spinner{
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-width: 2px!important;
|
||||
}
|
||||
</style>
|
||||
|
@ -18,6 +18,7 @@
|
||||
const myForm = form(username, password, confirm);
|
||||
|
||||
$: !$initialLoading && !!$isAuth && goto('/dashboard');
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<div class="parent">
|
||||
@ -62,7 +63,9 @@
|
||||
<button
|
||||
class="primary-btn"
|
||||
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);
|
||||
|
||||
errors = { ...errors, ...r.data.detail };
|
||||
@ -70,7 +73,12 @@
|
||||
}}
|
||||
disabled={!$myForm.valid}
|
||||
>
|
||||
S'inscrire
|
||||
<!-- loading et span.spinner else text-->
|
||||
{#if loading}
|
||||
<span class="spinner"></span>
|
||||
{:else}
|
||||
S'inscrire
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,4 +100,9 @@
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
.spinner{
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-width: 2px!important;
|
||||
}
|
||||
</style>
|
||||
|
@ -9,7 +9,6 @@ export const connect = (url,) => {
|
||||
const ws = new ReconnectingWebSocket(url);
|
||||
|
||||
ws.onmessage = (m) => {
|
||||
console.log('MESAGE', m)
|
||||
|
||||
messages.update((o) => [JSON.parse(m.data), ...o]);
|
||||
Object.values(get(handlers)).map(h => {
|
||||
|
Loading…
Reference in New Issue
Block a user