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