This commit is contained in:
Lilian 2023-02-26 11:35:37 +01:00 committed by Kilton937342
parent 90b48e710f
commit 9393c88f8e
20 changed files with 589 additions and 414 deletions

View File

@ -59,9 +59,15 @@ def change_room_status(room: Room, public: bool, db: Session):
return room
def delete_room_db(room: Room, db: Session):
db.delete(room)
db.commit()
return True
def get_member_from_user(user_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.user_id == user_id)).first()
room_id, Member.user_id == user_id)).first()
return member
@ -77,7 +83,7 @@ def get_member_from_token(token: str, room_id: int, db: Session):
def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session):
member = db.exec(select(Member).where(Member.room_id ==
room_id, Member.anonymous_id == anonymous_id)).first()
room_id, Member.anonymous_id == anonymous_id)).first()
return member
@ -109,7 +115,8 @@ def get_member_from_clientId(clientId: str, room_id: int, db: Session):
return member
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False, db: Session):
def create_member(*, room: Room, user: User | None = None, anonymous: Anonymous | None = None, waiting: bool = False,
db: Session):
member_id = generate_unique_code(Member, s=db)
member = Member(room=room, user=user, anonymous=anonymous, waiting=waiting,
id_code=member_id)
@ -154,7 +161,7 @@ def disconnect_member(member: Member, db: Session):
return member
def validate_username(username: str, room: Room, db: Session = Depends(get_session)):
def validate_username(username: str, room: Room, db: Session = Depends(get_session)):
if len(username) > 20:
return None
members = select(Member.anonymous_id).where(
@ -269,6 +276,8 @@ def refuse_waiter(member: Member, db: Session):
def leave_room(member: Member, db: Session):
#db.execute(delete(Challenger).where(col(Challenger.member_id) == member.id))
#db.execute(delete(TmpCorrection).where(col(TmpCorrection.member_id) == member.id))
db.delete(member)
db.commit()
return None
@ -525,7 +534,6 @@ def change_challengers_validation(p: Parcours, validation: int, db: Session):
def change_challenges_validation(p: Parcours, validation: int, db: Session):
challenges = db.exec(select(Challenge).where(
Challenge.parcours_id == p.id_code)).all()
print('CHALLS', challenges)
@ -833,7 +841,7 @@ def check_admin(member: Member = Depends(get_member_dep)):
def get_parcours(parcours_id: str, room: Room = Depends(get_room), db: Session = Depends(get_session)):
room = db.exec(select(Parcours).where(Parcours.id_code ==
parcours_id, Parcours.room_id == room.id)).first()
parcours_id, Parcours.room_id == room.id)).first()
if room is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Parcours introuvable")
@ -844,10 +852,12 @@ def get_exercices(parcours: Parcours = Depends(get_parcours), db: Session = Depe
exercices = db.exec(select(Exercice).where(col(Exercice.id_code).in_(
[e['exercice_id'] for e in parcours.exercices]))).all()
return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']} for e in exercices]
return [{"exercice": e, "quantity": [q for q in parcours.exercices if q['exercice_id'] == e.id_code][0]['quantity']}
for e in exercices]
def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep), db: Session = Depends(get_session)):
def get_correction(correction_id: str, parcours_id: str, member: Member = Depends(get_member_dep),
db: Session = Depends(get_session)):
tmpCorr = db.exec(select(TmpCorrection).where(
TmpCorrection.id_code == correction_id, TmpCorrection.parcours_id == parcours_id)).first()
if tmpCorr is None:

View File

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

Binary file not shown.

View File

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

View File

@ -8,30 +8,7 @@ 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, \
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 delete_room_db
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, \
@ -68,6 +45,14 @@ def get_room_route(room: Room = Depends(get_room), member: Member = Depends(get_
return serialize_room(room, member, db)
@router.delete('/room/{room_id}', dependencies=[ Depends(check_admin) ])
async def delete_room(room: Room = Depends(get_room), m: RoomManager = Depends(get_manager),
db: Session = Depends(get_session)):
delete_room_db(room, db)
await m.broadcast({"type": "deleted"}, room.id_code)
return {"message": "ok"}
@router.post('/room/{room_id}/parcours', response_model=ParcoursRead)
async def create_parcours(*, parcours: ParcoursCreate, room_id: str, member: Member = Depends(check_admin),
m: RoomManager = Depends(get_manager), db: Session = Depends(get_session)):
@ -258,8 +243,7 @@ async def corrige(*, correction: List[CorrigedData] = Body(), challenge: Challen
@router.websocket('/ws/room/{room_id}')
async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session),
m: RoomManager = Depends(get_manager)):
if room is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Room not found')
consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db)
await consumer.run()

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
afterNavigate(() => {
open = false;
});
$: console.log("USERNAME", $username);
</script>
<nav data-sveltekit-preload-data="hover" class:open>
@ -37,7 +38,8 @@
<div class="icon">
<FaUser />
</div>
{$username}</div>
{$username}
</div>
</NavLink>
<div class="icon signout" title="Se déconnecter" on:click={()=>{
logout()

View File

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

View File

@ -4,6 +4,10 @@
import FaLock from "svelte-icons/fa/FaLock.svelte";
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import FaTimes from "svelte-icons/fa/FaTimes.svelte";
import FaCheck from "svelte-icons/fa/FaCheck.svelte";
import MdCheck from "svelte-icons/md/MdCheck.svelte";
import MdClose from "svelte-icons/md/MdClose.svelte";
const room: Writable<Room> = getContext("room");
const member: Writable<Member> = getContext("member");
@ -111,14 +115,20 @@
class="accept"
on:click={() => {
send('accept', { waiter_id: m.waiter_id });
}}>Accept
}}
title="Accepter"
>
<MdCheck />
</button
>
<button
class="refuse"
on:click={() => {
send('refuse', { waiter_id: m.waiter_id });
}}>Refuse
}}
title="Refuser"
>
<MdClose />
</button
>
</p>
@ -171,6 +181,9 @@
font-size: 1em;
width: max-content;
display: flex;
align-items: center;
span {
color: grey;
font-size: 0.9em;
@ -203,4 +216,27 @@
font-weight: 500;
font-size: 0.8em !important;
}
.accept, .refuse {
border: none;
background-color: transparent;
cursor: pointer;
width: 30px;
height: 30px;
transition: .3s;
margin: 0;
&:hover {
transform: scale(1.1);
}
}
.accept {
margin-left: 5px;
color: $green;
}
.refuse {
color: $red;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import {authInstance} from '../apis/auth.api';
export const loginRequest = (data: { username: string; password: string }) => {
return authInstance
.request({
url: 'http://localhost:8002/login',
url: '/login',
method: 'POST',
data,
headers: {
@ -23,7 +23,7 @@ export const registerRequest = (data: {
}) => {
return authInstance
.request({
url: 'http://localhost:8002/register',
url: '/register',
method: 'POST',
data,
headers: {
@ -39,7 +39,7 @@ export const registerRequest = (data: {
export const refreshRequest = (token: string) => {
return authInstance
.request({
url: 'http://localhost:8002/refresh',
url: '/refresh',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',

View File

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

View File

@ -21,6 +21,7 @@
<Auth>
<Alert>
<Modal>
<main>
<NavBar/>
<slot/>

View File

@ -26,13 +26,13 @@
const room = writable<Room | null>(null);
const member = writable<Member | null>(null);
const parcours = writable<ParcoursRead | null>(null);
const ws = connect("ws://127.0.0.1:8002/ws/room/" + $page.params.slug);
const ws = connect("ws://127.0.0.1:8002/api/ws/room/" + $page.params.slug);
setContext("room", room);
setContext("member", member);
setContext("parcours", parcours);
setContext("ws", { send: (type: string, data: Object) => ws.send({ type, data }), ws: ws.ws });
const { error } = getContext("notif");
const { error, info } = getContext("notif");
const onMessage = (payload: { type: string; data: any }) => {
if (payload == undefined) return;
const { type, data } = payload;
@ -83,11 +83,13 @@
case "waiting":
close()
$member = { ...data.waiter, room: data.room };
goto(`?${new URLSearchParams({ a: "waiting" })}`);
return;
case "refused":
error("Refusé", "L'administrateur a refusé votre demande");
close();
ws?.close(1000);
goto("/room/join");
@ -107,11 +109,18 @@
}
return;
case "banned":
error("Ban", "Vous avez été banni de la salle par l'administrateur");
ws?.close(1000);
goto("/room/join");
sessionStorage.removeItem("reconnect");
return;
case "deleted":
info("Suppression", "La salle a été supprimée par l'administrateur");
ws.close(1000)
goto("/room/join")
return
case "error":
const { code, msg } = data;
if (code == 401) {
@ -148,6 +157,10 @@
} else {
error("Erreur", "Message : " + msg);
}
if(code == 404){
ws.close(1000)
goto("/room/join")
}
return;
case "waiter":
@ -507,6 +520,7 @@
column-gap: 50px;
row-gap: 10px;
height: 100%;
padding: 7px 15px;
@include down(800) {
display: flex;
flex-direction: column;