Prêt a déployer

This commit is contained in:
Benjamin 2023-02-26 16:29:05 +01:00 committed by Kilton937342
parent 9393c88f8e
commit 71038c169c
35 changed files with 1369 additions and 1073 deletions

View File

@ -5,7 +5,7 @@ from typing import List
from fastapi import Depends, HTTPException, status, Query from fastapi import Depends, HTTPException, status, Query
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import func from sqlalchemy import func
from sqlmodel import Session, delete, select, col from sqlmodel import Session, delete, select, col, update
from database.auth.crud import get_user_from_token from database.auth.crud import get_user_from_token
from database.auth.models import User from database.auth.models import User
@ -333,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()
@ -379,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)
@ -401,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):
@ -519,28 +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)
@ -549,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]):
@ -571,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
@ -582,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
@ -736,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)
@ -775,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:

View File

@ -145,6 +145,7 @@ class ParcoursReadUpdate(SQLModel):
class ChallengerInfo(BaseModel): class ChallengerInfo(BaseModel):
name: str name: str
id_code: str id_code: str
validated: bool = False
class ChallengerAverage(ChallengerInfo): class ChallengerAverage(ChallengerInfo):

Binary file not shown.

View File

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

View File

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

View File

@ -8,12 +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 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, \ 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
@ -73,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),
@ -88,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)])
@ -168,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}
@ -243,7 +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)):
consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db) consumer = RoomConsumer(ws=ws, room=room, manager=m, db=db)
await consumer.run() await consumer.run()

View File

@ -4,6 +4,7 @@
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
} }
.spinner { .spinner {
width: 30px; width: 30px;
height: 30px; height: 30px;
@ -20,6 +21,7 @@
.italic { .italic {
font-style: italic; font-style: italic;
} }
.underline { .underline {
text-decoration: underline; text-decoration: underline;
} }
@ -54,6 +56,7 @@
padding: 0 50px; padding: 0 50px;
width: max-content; width: max-content;
cursor: pointer; cursor: pointer;
&:disabled { &:disabled {
cursor: not-allowed cursor: not-allowed
} }
@ -62,6 +65,7 @@
.primary-btn { .primary-btn {
@extend .btn; @extend .btn;
background-color: #fcbf49; background-color: #fcbf49;
&:hover { &:hover {
background-color: #ac7b19; background-color: #ac7b19;
} }
@ -70,6 +74,7 @@
.danger-btn { .danger-btn {
@extend .btn; @extend .btn;
background-color: #fc5e49; background-color: #fc5e49;
&:hover { &:hover {
background-color: #ac1919; background-color: #ac1919;
} }
@ -80,6 +85,7 @@
background-color: transparent; background-color: transparent;
border: 1px solid #fcbf49; border: 1px solid #fcbf49;
color: #fcbf49; color: #fcbf49;
&:hover { &:hover {
background-color: #fcbf49; background-color: #fcbf49;
color: black; color: black;
@ -100,6 +106,7 @@
transition: 0.3s; transition: 0.3s;
border-radius: 0; border-radius: 0;
margin: 0; margin: 0;
&:focus { &:focus {
outline: none; outline: none;
border-bottom-color: $contrast; border-bottom-color: $contrast;
@ -111,6 +118,7 @@
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;
@ -126,3 +134,11 @@
font-weight: 900; font-weight: 900;
} }
.contrast {
color: $contrast;
}
.loading {
cursor: progress;
}

View File

@ -5,7 +5,7 @@
import FaHome from "svelte-icons/fa/FaHome.svelte"; import FaHome from "svelte-icons/fa/FaHome.svelte";
import { afterNavigate } from "$app/navigation"; import { afterNavigate } from "$app/navigation";
import FaUser from "svelte-icons/fa/FaUser.svelte"; import FaUser from "svelte-icons/fa/FaUser.svelte";
import FaSignOutAlt from "svelte-icons/fa/FaSignOutAlt.svelte"; import IoIosLogOut from 'svelte-icons/io/IoIosLogOut.svelte'
const { const {
isAuth, isAuth,
@ -30,6 +30,7 @@
<NavLink href="/room">Salles</NavLink> <NavLink href="/room">Salles</NavLink>
</div> </div>
<div class="right">
<div class="auth"> <div class="auth">
{#if $isAuth && $username != null} {#if $isAuth && $username != null}
@ -44,17 +45,17 @@
<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";
@ -109,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;
@ -166,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;
@ -186,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;
@ -222,4 +243,14 @@
padding-right: 20px; padding-right: 20px;
} }
@keyframes open {
0% {
gap: 10px;
opacity: .3;
}
100% {
gap: 42px;
opacity: 1;
}
}
</style> </style>

View File

@ -6,23 +6,23 @@
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}
@ -39,8 +39,13 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
div { div {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@include down(800px){
grid-template-columns: 1fr;
}
} }
</style> </style>

View File

@ -6,12 +6,12 @@
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>
@ -24,8 +24,12 @@
</div> </div>
{/if} {/if}
<style lang="scss"> <style lang="scss">
@import "../../mixins.scss";
div { div {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@include down(800){
grid-template-columns: 1fr;}
} }
</style> </style>

View File

@ -1,16 +1,17 @@
<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} />
</div>
{title}</h2> {title}</h2>
<div class="content"> <div class="content">
<slot /> <slot />
@ -24,10 +25,16 @@
</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;
@ -49,5 +56,8 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-top: 10px; margin-top: 10px;
@include down(800){
justify-content: center;
}
} }
</style> </style>

View File

@ -1,21 +1,21 @@
<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 = () => {
@ -92,9 +92,11 @@
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
* { * {
transition: 0.45s; transition: 0.45s;
} }
.icon { .icon {
width: 18px; width: 18px;
height: 18px; height: 18px;
@ -102,6 +104,7 @@
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;
transform: scale(0.9); transform: scale(0.9);
&:hover { &:hover {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
@ -124,6 +127,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-bottom: 20px; margin-bottom: 20px;
p { p {
margin: 10px; margin: 10px;
margin-left: 18px; margin-left: 18px;
@ -132,6 +136,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
h2 { h2 {
font-size: 0.95em; font-size: 0.95em;
margin: 10px; margin: 10px;
@ -147,16 +152,20 @@
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
border: 1px solid $border; border: 1px solid $border;
+ :global(div) { + :global(div) {
transition: 0.45s; transition: 0.45s;
} }
&:hover { &:hover {
border: 1px solid $primary; border: 1px solid $primary;
+ :global(div) { + :global(div) {
border: 1px solid $primary; border: 1px solid $primary;
border-top: none; border-top: none;
} }
} }
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75); box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
} }
@ -173,6 +182,7 @@
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-wrap: break-word; word-wrap: break-word;
&:hover { &:hover {
color: $primary; color: $primary;
} }
@ -186,6 +196,8 @@
background-color: $background; background-color: $background;
min-height: 250px; min-height: 250px;
max-height: 300px; max-height: 300px;
min-width: 250px;
&:hover { &:hover {
transform: translateX(10px) translateY(-10px); transform: translateX(10px) translateY(-10px);
} }

View File

@ -1,7 +1,7 @@
<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 }>;
@ -19,6 +19,8 @@
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
@import '../../mixins';
div { div {
background: $background; background: $background;
padding: 50px; padding: 50px;
@ -26,7 +28,15 @@
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
align-items: flex-start; align-items: flex-start;
@include down(800){
width: 100%;
height: 100%;
min-width: 0;
} }
min-width: 800px;
}
h1 { h1 {
font-size: 1.5em; font-size: 1.5em;
} }

View File

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

View File

@ -1,21 +1,25 @@
<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";
import ModalCard from "./ModalCard.svelte";
export let editing = true; export let editing = true;
export let updateExo: Function = (e: Exercice) => {}; export let updateExo: Function = (e: Exercice) => {
};
export let exo: Exercice | null = null; export let exo: Exercice | null = null;
export let cancel: Function; export let cancel: Function;
const { alert } = getContext<{ alert: Function }>('alert'); const { alert } = getContext<{ alert: Function }>("alert");
const { show } = getContext<{ show: Function }>("modal");
// "Legally" initiate empty FileList for model field (simple list raises warning) // "Legally" initiate empty FileList for model field (simple list raises warning)
/* let list = new DataTransfer(); /* let list = new DataTransfer();
@ -24,15 +28,19 @@
!editing && list.items.remove(0); */ !editing && list.items.remove(0); */
// Initiate fields and form // Initiate fields and form
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], { const name = field("name", !!exo ? exo.name : "", [required(), max(50), min(5)], {
checkOnInit: true checkOnInit: true
}); });
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true }); const consigne = field("consigne", !!exo && exo.consigne != null ? exo.consigne : "", [max(200)], { checkOnInit: true });
const prv = field('private', !!exo ? exo.private : false); const prv = field("private", !!exo ? exo.private : false);
const model = field('model', [], [checkFile(), required()], { const model = field("model", [], [checkFile(), required()], {
checkOnInit: !editing checkOnInit: !editing
}); });
const myForm = form(name, consigne, prv, model); 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");
</script> </script>
<form <form
@ -45,9 +53,13 @@
private: $prv.value, private: $prv.value,
...($model.dirty == true && { file: $model.value[0] }) ...($model.dirty == true && { file: $model.value[0] })
}).then((r) => { }).then((r) => {
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) => {
console.log(e)
error('Erreur', 'Une erreur est survenue lors de la modification de l\'exercice')
}); });
} else { } else {
createExo({ createExo({
@ -57,7 +69,22 @@
file: $model.value[0] file: $model.value[0]
}).then((r) => { }).then((r) => {
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) => {
error('Erreur', 'Une erreur est survenue lors de la création de l\'exercice')
}); });
} }
}} }}
@ -86,7 +113,8 @@
<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}>Modifier</button>
@ -104,7 +132,8 @@
} else { } else {
cancel(); cancel();
} }
}}>Annuler</button }}>Annuler
</button
> >
</div> </div>
</form> </form>

View File

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

View File

@ -112,12 +112,14 @@
</script> </script>
<div class="full" class:loading={$tagStore.isFetching || $exerciceStore.isFetching}>
{#if $tagStore.data != undefined} {#if $tagStore.data != undefined}
<Head location={filter} bind:search bind:selected /> <Head location={filter} bind:search bind:selected />
{/if} {/if}
{#if $tagStore.isFetching == true}
Fetching
{/if}
<div class="feed"> <div class="feed">
<div class="title"> <div class="title">
@ -143,7 +145,7 @@
<Card bind:exo={e} /> <Card bind:exo={e} />
{/each} {/each}
{#if $exerciceStore.data.items.length == 0} {#if $exerciceStore.data.items.length == 0}
<p>Aucun exercices</p> <p class="empty">Aucun exercice</p>
{/if} {/if}
{:else} {:else}
{#each Array(10) as i} {#each Array(10) as i}
@ -154,12 +156,15 @@
{#if $exerciceStore.data != undefined} {#if $exerciceStore.data != undefined}
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} /> <Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
{/if} {/if}
</div>
<style lang="scss"> <style lang="scss">
@import '../../variables'; @import '../../variables';
@import "../../mixins.scss";
.skeleton { .skeleton {
width: 330px; //width: 330px;
max-width: 330px;
height: 250px; height: 250px;
opacity: .8; opacity: .8;
@ -214,6 +219,10 @@
margin-top: 20px; margin-top: 20px;
} }
.full {
padding: 0 20px;
}
.title { .title {
grid-column: 1/3; grid-column: 1/3;
display: flex; display: flex;
@ -221,6 +230,11 @@
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@include down(600) {
grid-column: 1/2;
text-align: center;
}
h1 { h1 {
font-size: 3.5em; font-size: 3.5em;
font-weight: bolder; font-weight: bolder;
@ -235,4 +249,11 @@
color: $primary; color: $primary;
} }
} }
.empty{
display: flex;
justify-content: center;
align-items: center;
font-style: italic;
}
</style> </style>

View File

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

View File

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

View File

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

View File

@ -5,18 +5,18 @@
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[];
@ -43,13 +43,12 @@
remaining = p.time; remaining = p.time;
}); });
} }
}) });
let timer: number | null = null; let timer: number | null = null;
let remaining: number | null = null; let remaining: number | null = null;
const {error, success} = getContext("notif");
$: { $: {
if (!corrige && $challengeStore != null && remaining == null) { if (!corrige && $challengeStore != null && remaining == null) {
remaining = $challengeStore.parcours.time * 60; remaining = $challengeStore.parcours.time * 60;
@ -71,6 +70,7 @@
}); });
</script> </script>
<div class="full">
{#if $challengeStore != null} {#if $challengeStore != null}
<div class="head"> <div class="head">
<h1> <h1>
@ -78,7 +78,7 @@
{#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
> >
@ -102,9 +102,11 @@
> >
{/if} {/if}
</h1> </h1>
{#if $challengeStore.mistakes} {#if $challengeStore.mistakes}
{$challengeStore.mistakes} fautes <p class="mistakes" class:validated={$challengeStore.validated}>{$challengeStore.mistakes} fautes</p>
{/if} {/if}
{#if !corrige} {#if !corrige}
<p <p
class="timer" class="timer"
@ -121,9 +123,11 @@
<div class="exo"> <div class="exo">
<div class="infos"> <div class="infos">
<h2>Exercice {d + 1} : <span>{e.exo.name}</span></h2> <h2>Exercice {d + 1} : <span>{e.exo.name}</span></h2>
{#if e.exo.consigne != null}
<p> <p>
{e.exo.consigne} {e.exo.consigne}
</p> </p>
{/if}
</div> </div>
<div class="data"> <div class="data">
{#each e.data as c, a} {#each e.data as c, a}
@ -175,7 +179,8 @@
clearInterval(timer); clearInterval(timer);
} }
}); });
}}>Valider !</button }}>Valider !
</button
> >
<button <button
hidden={!$challengeStore.corriged} hidden={!$challengeStore.corriged}
@ -190,7 +195,8 @@
timer = null; timer = null;
} }
}); });
}}>Réessayer !</button }}>Réessayer !
</button
> >
{:else if $member.isAdmin} {:else if $member.isAdmin}
<button <button
@ -202,9 +208,12 @@
$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}
@ -217,8 +226,14 @@
> >
</div> </div>
{/if} {/if}
</div>
<style lang="scss"> <style lang="scss">
@import "../../mixins";
.full {
padding: 7px 15px;
}
.timer { .timer {
font-size: 2em; font-size: 2em;
color: $green; color: $green;
@ -235,6 +250,20 @@
justify-content: space-between; justify-content: space-between;
margin: 40px 0; margin: 40px 0;
min-height: 70px; min-height: 70px;
@include down(800px) {
flex-direction: column;
text-align: center;
h1{
flex-direction: column;
}
}
@include up(800px) {
.correction-info::before{
content: " - ";
}
}
} }
.late { .late {
@ -247,6 +276,7 @@
gap: 10px; gap: 10px;
position: relative; position: relative;
} }
.data { .data {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@ -256,15 +286,18 @@
.infos { .infos {
h2 { h2 {
font-size: 1.2em; font-size: 1.2em;
span { span {
font-style: italic; font-style: italic;
} }
} }
p { p {
font-size: 1em; font-size: 1em;
font-style: italic; font-style: italic;
text-decoration: underline; text-decoration: underline;
} }
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -272,25 +305,39 @@
margin-bottom: 30px; margin-bottom: 30px;
margin-top: 20px; margin-top: 20px;
} }
.icon { .icon {
height: 20px; height: 20px;
width: 20px; width: 20px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
transition: 0.2s; transition: 0.2s;
&:hover { &:hover {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
h1 { h1 {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
font-size: 2.3em; font-size: 2.3em;
} }
.correction-info { .correction-info {
font-size: 0.6em; font-size: 0.6em;
color: grey; color: grey;
font-weight: 600; font-weight: 600;
} }
.mistakes {
font-weight: 700;
font-size: 2em;
color: $red;
}
.validated {
color: $green;
}
</style> </style>

View File

@ -1,16 +1,17 @@
<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}
@ -30,6 +31,7 @@
> >
<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}
<span class:valid = {chall.challenger.validated} class="validation-status">{chall.challenger.validated ? "Validé": "Non validé"}</span>
</p> </p>
{#if selected == chall.challenger.id_code} {#if selected == chall.challenger.id_code}
@ -43,7 +45,7 @@
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>
@ -69,8 +71,10 @@
transition: 0.2s; transition: 0.2s;
} }
} }
.selected { .selected {
font-weight: 700; font-weight: 700;
.icon { .icon {
transform: rotate(0); transform: rotate(0);
} }
@ -82,7 +86,8 @@
cursor: pointer; cursor: pointer;
width: max-content; width: max-content;
margin-left: 30px; margin-left: 30px;
p span{
p > span {
color: $red; color: $red;
font-weight: 600; font-weight: 600;
} }
@ -102,12 +107,28 @@
color: grey; color: grey;
font-weight: 900; font-weight: 900;
} }
.trylist { .trylist {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.validated { .validation-status{
color: $red;
font-weight: 600;
&::before{
content: " - ";
}
}
.validated, .valid {
color: $green !important; color: $green !important;
} }
.time-overflow{
color: $red;
font-weight: 400;
font-size: .8em;
}
</style> </style>

View File

@ -4,8 +4,7 @@
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 FaSignOutAlt from "svelte-icons/fa/FaSignOutAlt.svelte"; import IoMdLogOut from 'svelte-icons/io/IoMdLogOut.svelte'
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { Member, Room } from "../../types/room.type"; import type { Member, Room } from "../../types/room.type";
@ -84,7 +83,7 @@
}} }}
on:keypress={()=>{}} on:keypress={()=>{}}
> >
<FaSignOutAlt /> <IoMdLogOut />
</div> </div>
{/if} {/if}

View File

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

View File

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

View File

@ -1,16 +1,17 @@
<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 = { type Notif = {
title: string; title: string;
description: string; description: string;
type: 'alert' | 'info' | 'success' | 'error'; type: "alert" | "info" | "success" | "error";
}; };
type Notification = { type Notification = {
title: string; title: string;
description: string; description: string;
type: 'alert' | 'info' | 'success' | 'error'; type: "alert" | "info" | "success" | "error";
id: number; id: number;
deleted: boolean; deleted: boolean;
}; };
@ -44,18 +45,18 @@
}; };
const alert = (title: string, description: string) => { const alert = (title: string, description: string) => {
toast({ title, description, type: 'alert' }); toast({ title, description, type: "alert" });
}; };
const info = (title: string, description: string) => { const info = (title: string, description: string) => {
toast({ title, description, type: 'info' }); toast({ title, description, type: "info" });
}; };
const success = (title: string, description: string) => { const success = (title: string, description: string) => {
toast({ title, description, type: 'success' }); toast({ title, description, type: "success" });
}; };
const error = (title: string, description: string) => { const error = (title: string, description: string) => {
toast({ title, description, type: 'error' }); toast({ title, description, type: "error" });
}; };
setContext('notif', { toast, alert, info, success, error }); setContext("notif", { toast, alert, info, success, error });
</script> </script>
<slot /> <slot />
@ -74,7 +75,9 @@
class:deleted={n.deleted} class:deleted={n.deleted}
> >
<h1>{n.title}</h1> <h1>{n.title}</h1>
<p><SvelteMarkdown source={n.description} /></p> <p>
<SvelteMarkdown source={n.description} />
</p>
</div> </div>
{/each} {/each}
</div> </div>
@ -84,12 +87,12 @@
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;
@ -99,9 +102,11 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
word-wrap: break-word; word-wrap: break-word;
h1 { h1 {
font-size: 1.1em; font-size: 1.1em;
} }
p { p {
font-size: 0.9em; font-size: 0.9em;
} }
@ -116,6 +121,7 @@
right: 0; right: 0;
left: 0; left: 0;
} }
&::before { &::before {
z-index: 3; z-index: 3;
background-color: $background-light; background-color: $background-light;
@ -123,32 +129,40 @@
width: 0%; width: 0%;
animation: slide 3s forwards ease-in-out; animation: slide 3s forwards ease-in-out;
} }
&::after { &::after {
width: 100%; width: 100%;
z-index: 2; z-index: 2;
} }
&.deleted { &.deleted {
transition: 0.5s; transition: 0.5s;
opacity: 0; opacity: 0;
} }
} }
} }
.empty { .empty {
z-index: -20; z-index: -20;
} }
.alert::after { .alert::after {
background-color: $orange; background-color: $orange;
} }
.info::after { .info::after {
background-color: $bleu; background-color: $bleu;
} }
.error::after { .error::after {
background-color: $red; background-color: $red;
} }
.success::after { .success::after {
background-color: $green; background-color: $green;
} }
@keyframes slide { @keyframes slide {
from { from {
width: 0%; width: 0%;

View File

@ -1,5 +1,6 @@
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
@ -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: '/refresh', url: `${env.PUBLIC_API_BASE}/refresh`,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',

View File

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

View File

@ -4,7 +4,7 @@ import { roomInstance } from '../apis/room.api';
export const createRoom = (data: { name: string }, username: string | null = null) => { export const createRoom = (data: { name: string }, username: string | null = null) => {
return roomInstance return roomInstance
.request({ .request({
url: '/', url: '',
method: 'POST', method: 'POST',
params: { username }, params: { username },
data: { ...data, public: false, global_results: false } data: { ...data, public: false, global_results: false }

View File

@ -79,7 +79,7 @@
height: calc(100vh - var(--navbar-height) - 10px); height: calc(100vh - var(--navbar-height) - 10px);
overflow: auto; overflow: auto;
height: 100%; height: 100%;
overflow-x: hidden;
a { a {
color: red; color: red;
} }

View File

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

View File

@ -1,4 +1,4 @@
<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";
@ -10,26 +10,26 @@
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>
@ -71,9 +71,18 @@
</div> </div>
<style lang='scss'> <style lang="scss">
@import "../../mixins.scss";
h1 { h1 {
font-size: 3em; font-size: 3em;
margin-bottom: 20px; margin-bottom: 20px;
@include down(800){
text-align: center;
}
}
div {
padding: 7px 15px;
} }
</style> </style>

View File

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

View File

@ -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,7 +82,7 @@
case "waiting": case "waiting":
close() 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;
@ -117,9 +116,9 @@
case "deleted": case "deleted":
info("Suppression", "La salle a été supprimée par l'administrateur"); info("Suppression", "La salle a été supprimée par l'administrateur");
ws.close(1000) ws.close(1000);
goto("/room/join") goto("/room/join");
return return;
case "error": case "error":
const { code, msg } = data; const { code, msg } = data;
@ -158,8 +157,8 @@
error("Erreur", "Message : " + msg); error("Erreur", "Message : " + msg);
} }
if (code == 404) { if (code == 404) {
ws.close(1000) ws.close(1000);
goto("/room/join") goto("/room/join");
} }
return; return;
case "waiter": case "waiter":
@ -360,7 +359,7 @@
} }
} }
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) => {
@ -369,9 +368,25 @@
} }
return c; return c;
}); });
$parcours.challenges[data.member].challenger.validated = data.validated;
if (data.member == $member.id_code) {
$parcours.validated = data.validated; $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;
}
}; };
handlers.update((h) => { handlers.update((h) => {