before style
This commit is contained in:
parent
ccd047dbb0
commit
43031d22fb
@ -2,7 +2,7 @@ from fastapi import Query, HTTPException, status
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from typing import IO, List
|
from typing import IO, List
|
||||||
from sqlmodel import Session, select, or_
|
from sqlmodel import Session, select, or_, col
|
||||||
from generateur.generateur_main import generate_from_data, generate_from_path
|
from generateur.generateur_main import generate_from_data, generate_from_path
|
||||||
from database.auth.models import User
|
from database.auth.models import User
|
||||||
from database.db import get_session
|
from database.db import get_session
|
||||||
@ -62,22 +62,23 @@ def clone_exo_db(exercice: Exercice, user: User, db: Session):
|
|||||||
db.refresh(new_exo)
|
db.refresh(new_exo)
|
||||||
return new_exo
|
return new_exo
|
||||||
|
|
||||||
def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit,supports: Supports, exo_source: IO | None, db: Session):
|
def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit, supports: Supports | None, exo_source: IO | None, db: Session):
|
||||||
|
|
||||||
exo_data = new_exo.dict(exclude_unset=True, exclude_none=True)
|
exo_data = new_exo.dict(exclude_unset=True, exclude_none=True)
|
||||||
|
|
||||||
for key, value in exo_data.items():
|
for key, value in exo_data.items():
|
||||||
setattr(old_exo, key, value)
|
setattr(old_exo, key, value)
|
||||||
|
|
||||||
old_exo.csv = supports['csv']
|
if supports is not None:
|
||||||
old_exo.pdf = supports['pdf']
|
old_exo.csv = supports['csv']
|
||||||
old_exo.web = supports['web']
|
old_exo.pdf = supports['pdf']
|
||||||
|
old_exo.web = supports['web']
|
||||||
|
|
||||||
example = {
|
example = {
|
||||||
"type": ExampleEnum.csv if supports['csv'] == True else ExampleEnum.web if supports['web'] == True else None,
|
"type": ExampleEnum.csv if supports['csv'] == True else ExampleEnum.web if supports['web'] == True else None,
|
||||||
"data": generate_from_data(exo_source.read(), 3, "csv" if supports['csv'] == True else "web" if supports['web'] == True else None, True) if supports['csv'] == True == True or supports['web'] == True == True else None
|
"data": generate_from_data(exo_source.read(), 3, "csv" if supports['csv'] == True else "web" if supports['web'] == True else None, True) if supports['csv'] == True == True or supports['web'] == True == True else None
|
||||||
}
|
}
|
||||||
old_exo.examples = example
|
old_exo.examples = example
|
||||||
|
|
||||||
if exo_source:
|
if exo_source:
|
||||||
os.remove(add_fast_api_root(old_exo.exo_source))
|
os.remove(add_fast_api_root(old_exo.exo_source))
|
||||||
@ -87,7 +88,6 @@ def update_exo_db(old_exo: Exercice, new_exo: ExerciceEdit,supports: Supports, e
|
|||||||
db.refresh(old_exo)
|
db.refresh(old_exo)
|
||||||
return old_exo
|
return old_exo
|
||||||
|
|
||||||
|
|
||||||
def delete_exo_db(exo: Exercice, db: Session):
|
def delete_exo_db(exo: Exercice, db: Session):
|
||||||
db.delete(exo)
|
db.delete(exo)
|
||||||
db.commit()
|
db.commit()
|
||||||
@ -98,24 +98,27 @@ def get_or_create_tag(tag: TagCreate, user: User, db: Session):
|
|||||||
tag_db = db.exec(select(Tag).where(Tag.author_id == user.id).where(or_(
|
tag_db = db.exec(select(Tag).where(Tag.author_id == user.id).where(or_(
|
||||||
Tag.id_code == tag.id_code, Tag.label == tag.label))).first()
|
Tag.id_code == tag.id_code, Tag.label == tag.label))).first()
|
||||||
if tag_db is not None:
|
if tag_db is not None:
|
||||||
return tag_db
|
return tag_db, False
|
||||||
id_code = generate_unique_code(Tag, db)
|
id_code = generate_unique_code(Tag, db)
|
||||||
tag_db = Tag(**{**tag.dict(exclude_unset=True),
|
tag_db = Tag(**{**tag.dict(exclude_unset=True),
|
||||||
'id_code': id_code, 'author_id': user.id})
|
'id_code': id_code, 'author_id': user.id})
|
||||||
db.add(tag_db)
|
db.add(tag_db)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(tag_db)
|
db.refresh(tag_db)
|
||||||
return tag_db
|
return tag_db, True
|
||||||
|
|
||||||
|
|
||||||
def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session):
|
def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session):
|
||||||
|
new = []
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
tag_db = get_or_create_tag(tag, user, db)
|
tag_db, created = get_or_create_tag(tag, user, db)
|
||||||
|
if created:
|
||||||
|
new.append(tag_db)
|
||||||
exo.tags.append(tag_db)
|
exo.tags.append(tag_db)
|
||||||
db.add(exo)
|
db.add(exo)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(exo)
|
db.refresh(exo)
|
||||||
return exo
|
return exo, new
|
||||||
|
|
||||||
|
|
||||||
def remove_tag_db(exo: Exercice, tag: Tag, db: Session):
|
def remove_tag_db(exo: Exercice, tag: Tag, db: Session):
|
||||||
@ -171,13 +174,11 @@ def check_tag_author(tag_id: str, user: User = Depends(get_current_user), db: Se
|
|||||||
|
|
||||||
def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depends(get_session)):
|
def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depends(get_session)):
|
||||||
if tags is None:
|
if tags is None:
|
||||||
return None
|
return []
|
||||||
validated_tags = []
|
validated_tags = db.exec(
|
||||||
for t in tags:
|
select(Tag.id_code, Tag.id).where(col(Tag.id_code).in_(tags))).all()
|
||||||
tag = db.exec(select(Tag.id).where(Tag.id_code == t)).first()
|
|
||||||
if tag is not None:
|
return [t.id for t in validated_tags]
|
||||||
validated_tags.append(tag)
|
|
||||||
return validated_tags
|
|
||||||
|
|
||||||
|
|
||||||
#Serialize
|
#Serialize
|
||||||
@ -185,9 +186,17 @@ def get_tags_dependency(tags: List[str] | None = Query(None), db: Session = Depe
|
|||||||
def check_author(exo: Exercice, user_id:int):
|
def check_author(exo: Exercice, user_id:int):
|
||||||
return exo.author_id == user_id
|
return exo.author_id == user_id
|
||||||
|
|
||||||
def serialize_exo(*, exo: ExerciceRead, user_id: User = None, db: Session):
|
def serialize_exo(*, exo: Exercice, user_id: User = None, db: Session):
|
||||||
tags = parse_exo_tags(exo_id=exo.id, user_id=user_id,
|
tags = parse_exo_tags(exo_id=exo.id, user_id=user_id,
|
||||||
db=db) if user_id is not None else []
|
db=db) if user_id is not None else []
|
||||||
is_author = user_id is not None and check_author(exo=exo, user_id=user_id)
|
is_author = user_id is not None and check_author(exo=exo, user_id=user_id)
|
||||||
return ExerciceReadFull(**exo.dict(), author=exo.author, original=exo.original, tags=tags, is_author=is_author, supports={**exo.dict()})
|
print('USER', exo.dict(), exo)
|
||||||
|
if exo.original is not None:
|
||||||
|
print('TEST', db.exec(select(User).where(User.id == exo.original.author_id)).all())
|
||||||
|
author = db.exec(select(User).where(
|
||||||
|
User.id == exo.original.author_id)).all()[0]
|
||||||
|
original = {**exo.original.dict(), 'author': author.username}
|
||||||
|
else:
|
||||||
|
original = None
|
||||||
|
return ExerciceReadFull(**{**exo.dict(), "author":exo.author, "original":original, "tags":tags, "is_author":is_author, "supports":{**exo.dict()}})
|
||||||
|
|
||||||
|
@ -125,11 +125,6 @@ class ExerciceEdit(ExerciceCreate):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExerciceOrigin(SQLModel):
|
|
||||||
#id: int
|
|
||||||
id_code: str
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -138,6 +133,13 @@ class Author(SQLModel):
|
|||||||
username: str
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class ExerciceOrigin(SQLModel):
|
||||||
|
#id: int
|
||||||
|
id_code: str
|
||||||
|
name: str
|
||||||
|
author: str
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -164,12 +166,15 @@ class ExerciceReadBase(ExerciceBase):
|
|||||||
return get_filename_from_path(value)
|
return get_filename_from_path(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class ExerciceRead(ExerciceBase):
|
class ExerciceRead(ExerciceBase, Supports):
|
||||||
|
id_code: str
|
||||||
id: int
|
id: int
|
||||||
author_id:int
|
author_id:int
|
||||||
|
exo_source: str
|
||||||
author: User
|
author: User
|
||||||
original: Optional[Exercice]
|
original: Optional[Exercice]
|
||||||
tags: List[Tag]
|
tags: List[Tag]
|
||||||
|
examples: Example
|
||||||
|
|
||||||
class ExerciceReadFull(ExerciceReadBase):
|
class ExerciceReadFull(ExerciceReadBase):
|
||||||
supports: Supports
|
supports: Supports
|
||||||
|
12
backend/api/exo.py
Normal file
12
backend/api/exo.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import requests
|
||||||
|
def exos(nb, username, password):
|
||||||
|
rr = requests.post('http://localhost:8002/register', data={"username": username,
|
||||||
|
'password': password, 'password_confirm': password})
|
||||||
|
token = rr.json()['access_token']
|
||||||
|
for i in range(nb):
|
||||||
|
r = requests.post('http://localhost:8002/exercices', data={"name": "FakingTest" + str(i), "consigne": "consigne", "private": False}, files={
|
||||||
|
'file': ('test.py', open('./tests/testing_exo_source/exo_source.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||||
|
print('DONE')
|
||||||
|
|
||||||
|
exos(100, "lilianTest", "Pomme937342")
|
@ -37,6 +37,7 @@ origins = [
|
|||||||
"https://localhost:8001",
|
"https://localhost:8001",
|
||||||
"http://localhost",
|
"http://localhost",
|
||||||
"http://localhost:8080",
|
"http://localhost:8080",
|
||||||
|
"http://localhost:5173"
|
||||||
]
|
]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@ -45,6 +46,7 @@ app.add_middleware(
|
|||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
expose_headers=['*']
|
||||||
)
|
)
|
||||||
|
|
||||||
admin = Admin(app, engine)
|
admin = Admin(app, engine)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
|
from fastapi import APIRouter, Depends, Path, Query, UploadFile, HTTPException, status
|
||||||
@ -11,8 +12,10 @@ from services.exoValidation import validate_file, validate_file_optionnal
|
|||||||
from services.io import add_fast_api_root, get_filename_from_path
|
from services.io import add_fast_api_root, get_filename_from_path
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from sqlmodel import func
|
from sqlmodel import func
|
||||||
from fastapi_pagination import Page, paginate
|
from fastapi_pagination import paginate ,Page
|
||||||
|
from services.models import Page
|
||||||
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
from fastapi_pagination.ext.sqlalchemy_future import paginate as p
|
||||||
|
|
||||||
router = APIRouter(tags=['exercices'])
|
router = APIRouter(tags=['exercices'])
|
||||||
|
|
||||||
class ExoType(str, Enum):
|
class ExoType(str, Enum):
|
||||||
@ -27,7 +30,7 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
|||||||
return valid_exos
|
return valid_exos
|
||||||
|
|
||||||
|
|
||||||
def queryFilters_dependency(search: str = "", tags: List[int] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)):
|
def queryFilters_dependency(search: str = "", tags: List[str] | None = Depends(get_tags_dependency), type: ExoType | None = Query(default = None)):
|
||||||
return search, tags, type
|
return search, tags, type
|
||||||
|
|
||||||
|
|
||||||
@ -53,15 +56,10 @@ def clone_exo(exercice: Exercice | None = Depends(check_private), user: User = D
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull])
|
@router.get('/exercices/user', response_model=Page[ExerciceRead|ExerciceReadFull])
|
||||||
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
|
def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tuple[str, List[int] | None, ExoType | None] = Depends(queryFilters_dependency), db: Session = Depends(get_session)):
|
||||||
search, tags, type = queryFilters
|
search, tags, type = queryFilters
|
||||||
|
|
||||||
if tags is not None and len(tags) != 0:
|
statement = select(Exercice)
|
||||||
statement = select(Exercice, func.group_concat(
|
|
||||||
ExercicesTagLink.tag_id))
|
|
||||||
else:
|
|
||||||
statement = select(Exercice)
|
|
||||||
|
|
||||||
statement = statement.where(Exercice.author_id == user.id)
|
statement = statement.where(Exercice.author_id == user.id)
|
||||||
statement = statement.where(Exercice.name.startswith(search))
|
statement = statement.where(Exercice.name.startswith(search))
|
||||||
|
|
||||||
@ -72,18 +70,14 @@ def get_user_exercices(user: User = Depends(get_current_user), queryFilters: tup
|
|||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
|
|
||||||
if tags is not None and len(tags) != 0:
|
for t in tags:
|
||||||
statement = statement.join(ExercicesTagLink).where(
|
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
|
||||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
ExercicesTagLink.tag_id == t).exists()
|
||||||
|
statement = statement.where(sub)
|
||||||
#exercices = db.exec(statement).all()
|
|
||||||
page = p(db, statement)
|
page = p(db, statement)
|
||||||
exercices = page.items
|
exercices = page.items
|
||||||
if tags is not None and len(tags) != 0:
|
|
||||||
exercices = filter_exo_by_tags(exercices, tags)
|
|
||||||
page.items = [
|
page.items = [
|
||||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
@ -92,12 +86,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
search, tags, type = queryFilters
|
search, tags, type = queryFilters
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
if tags is not None and len(tags) != 0:
|
statement = select(Exercice)
|
||||||
statement = select(Exercice, func.group_concat(
|
|
||||||
ExercicesTagLink.tag_id))
|
|
||||||
else:
|
|
||||||
statement = select(Exercice)
|
|
||||||
|
|
||||||
statement = statement.where(Exercice.author_id != user.id)
|
statement = statement.where(Exercice.author_id != user.id)
|
||||||
statement = statement.where(Exercice.private == False)
|
statement = statement.where(Exercice.private == False)
|
||||||
statement = statement.where(Exercice.name.startswith(search))
|
statement = statement.where(Exercice.name.startswith(search))
|
||||||
@ -108,19 +97,19 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
statement = statement.where(Exercice.pdf == True)
|
statement = statement.where(Exercice.pdf == True)
|
||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
|
|
||||||
|
for t in tags:
|
||||||
|
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
|
||||||
|
ExercicesTagLink.tag_id == t).exists()
|
||||||
|
statement = statement.where(sub)
|
||||||
|
|
||||||
if tags is not None and len(tags) != 0:
|
page = p(db, statement)
|
||||||
statement = statement.join(ExercicesTagLink).where(
|
print('¨PAGE', page)
|
||||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
exercices = page.items
|
||||||
|
page.items = [
|
||||||
exercices = db.exec(statement).all()
|
|
||||||
|
|
||||||
if tags is not None and len(tags) != 0:
|
|
||||||
exercices = filter_exo_by_tags(exercices, tags)
|
|
||||||
|
|
||||||
exercices = [
|
|
||||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||||
return exercices
|
return page
|
||||||
|
|
||||||
else:
|
else:
|
||||||
statement = select(Exercice)
|
statement = select(Exercice)
|
||||||
statement = statement.where(Exercice.private == False)
|
statement = statement.where(Exercice.private == False)
|
||||||
@ -131,8 +120,12 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
|||||||
statement = statement.where(Exercice.pdf == True)
|
statement = statement.where(Exercice.pdf == True)
|
||||||
if type == ExoType.web:
|
if type == ExoType.web:
|
||||||
statement = statement.where(Exercice.web == True)
|
statement = statement.where(Exercice.web == True)
|
||||||
exercices = db.exec(statement).all()
|
|
||||||
return paginate([serialize_exo(exo=e, user_id=None, db=db) for e in exercices])
|
page = p(db, statement)
|
||||||
|
exercices = page.items
|
||||||
|
page.items = [
|
||||||
|
serialize_exo(exo=e, user_id=None, db=db) for e in exercices]
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
@router.get('/exercice/{id_code}', response_model=ExerciceReadFull)
|
@router.get('/exercice/{id_code}', response_model=ExerciceReadFull)
|
||||||
@ -153,7 +146,7 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
|
|||||||
if file:
|
if file:
|
||||||
file_obj = file["file"].file._file
|
file_obj = file["file"].file._file
|
||||||
file_obj.name = file['file'].filename
|
file_obj.name = file['file'].filename
|
||||||
exo_obj = update_exo_db(exo, exercice,file['supports'] if file is not None else None, file_obj, db)
|
exo_obj = update_exo_db(exo, exercice, file['supports'] if file is not None else None, file_obj, db)
|
||||||
return serialize_exo(exo=exo_obj, user_id=exo_obj.author_id, db=db)
|
return serialize_exo(exo=exo_obj, user_id=exo_obj.author_id, db=db)
|
||||||
|
|
||||||
|
|
||||||
@ -169,13 +162,19 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au
|
|||||||
return {'detail': 'Exercice supprimé avec succès'}
|
return {'detail': 'Exercice supprimé avec succès'}
|
||||||
|
|
||||||
|
|
||||||
@router.post('/exercice/{id_code}/tags', response_model=ExerciceReadFull, tags=['tags'])
|
|
||||||
|
class NewTags(BaseModel):
|
||||||
|
exo: ExerciceReadFull
|
||||||
|
tags: list[TagRead]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/exercice/{id_code}/tags', response_model=NewTags, tags=['tags'])
|
||||||
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
def add_tags(tags: List[TagCreate], exo: Exercice | None = Depends(get_exo_dependency), db: Session = Depends(get_session), user: User = Depends(get_current_user)):
|
||||||
if exo is None:
|
if exo is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||||
exo_obj = add_tags_db(exo, tags, user, db)
|
exo_obj, new = add_tags_db(exo, tags, user, db)
|
||||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
return {"exo":serialize_exo(exo=exo_obj, user_id=user.id, db=db), "tags": new}
|
||||||
|
|
||||||
|
|
||||||
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags'])
|
@router.delete('/exercice/{id_code}/tags/{tag_id}', response_model=ExerciceReadFull, tags=['tags'])
|
||||||
|
42
backend/api/services/models.py
Normal file
42
backend/api/services/models.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from typing import TypeVar, Generic, Sequence
|
||||||
|
|
||||||
|
from fastapi_pagination import Params
|
||||||
|
from fastapi_pagination.bases import AbstractPage, AbstractParams, BasePage
|
||||||
|
from math import ceil
|
||||||
|
from pydantic import conint
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
class Page(BasePage[T], Generic[T]):
|
||||||
|
page: conint(ge=1) # type: ignore
|
||||||
|
size: conint(ge=1)
|
||||||
|
totalPage: conint(ge=0)
|
||||||
|
hasMore: bool
|
||||||
|
|
||||||
|
|
||||||
|
__params_type__ = Params # Set params related to Page
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
items: Sequence[T],
|
||||||
|
total: int,
|
||||||
|
params: AbstractParams,
|
||||||
|
) -> Page[T]:
|
||||||
|
print("PARAMS", params)
|
||||||
|
totalPage = ceil(total/params.size)
|
||||||
|
return cls(
|
||||||
|
total=total,
|
||||||
|
items=items,
|
||||||
|
page=params.page,
|
||||||
|
size=params.size,
|
||||||
|
totalPage = totalPage,
|
||||||
|
hasMore= params.page < totalPage
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"size": params,
|
||||||
|
}
|
||||||
|
return cls(results=items)
|
@ -6,7 +6,6 @@ from tests.test_auth import test_register
|
|||||||
|
|
||||||
|
|
||||||
def test_create(client: TestClient, name="test_exo", consigne="consigne", private=False, user=None):
|
def test_create(client: TestClient, name="test_exo", consigne="consigne", private=False, user=None):
|
||||||
|
|
||||||
if user == None:
|
if user == None:
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
username = 'lilian'
|
username = 'lilian'
|
||||||
@ -145,6 +144,18 @@ def test_update(client: TestClient):
|
|||||||
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
|
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
def test_update_no_file(client: TestClient):
|
||||||
|
token = test_register(client, username="lilian")['access']
|
||||||
|
id_code = test_create(client, user={'token': token, 'username': "lilian"},
|
||||||
|
consigne="testconsigne")['id_code']
|
||||||
|
r = client.put('/exercice/' + id_code, data={"name": "name", "private": True}, headers={"Authorization": "Bearer " + token})
|
||||||
|
print(r.json())
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert 'id_code' in r.json()
|
||||||
|
assert r.json() == {'name': "name", 'consigne': "testconsigne", 'private': True, 'id_code': id_code, 'author': {'username': 'lilian'}, 'original': None, 'tags': [], 'exo_source': 'test.py', 'supports': {
|
||||||
|
'pdf': False, 'csv': True, 'web': True}, 'examples': {'type': 'csv', 'data': [{'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}, {'calcul': '1 + ... = 2', "correction": "1 + [1] = 2"}]}, 'is_author': True}
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
def test_update_missing_name(client: TestClient):
|
def test_update_missing_name(client: TestClient):
|
||||||
token = test_register(client, username="lilian")['access']
|
token = test_register(client, username="lilian")['access']
|
||||||
@ -346,19 +357,30 @@ def test_get_users_exos_page(client: TestClient):
|
|||||||
token1 = test_register(client, username="lilian")['access']
|
token1 = test_register(client, username="lilian")['access']
|
||||||
token2 = test_register(client, username="lilian2")['access']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
|
|
||||||
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv = test_create(client, private=True, user={
|
||||||
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv2 = test_create(client, private=True, user={
|
||||||
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv3 = test_create(client, private=True, user={
|
||||||
prv6= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv7= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv4 = test_create(client, private=True, user={
|
||||||
prv8= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv9= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv5 = test_create(client, private=True, user={
|
||||||
prv10= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv11 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv6 = test_create(client, private=True, user={
|
||||||
prv12 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv7 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv8 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv9 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv10 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv11 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv12 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
|
||||||
r = client.get('/exercices/user',
|
r = client.get('/exercices/user',
|
||||||
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
|
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
|
||||||
@ -366,24 +388,36 @@ def test_get_users_exos_page(client: TestClient):
|
|||||||
assert r.json()['page'] == 2
|
assert r.json()['page'] == 2
|
||||||
assert r.json()['size'] == 10
|
assert r.json()['size'] == 10
|
||||||
assert r.json()["items"] == [prv11, prv12]
|
assert r.json()["items"] == [prv11, prv12]
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_exos_page_up(client: TestClient):
|
def test_get_users_exos_page_up(client: TestClient):
|
||||||
token1 = test_register(client, username="lilian")['access']
|
token1 = test_register(client, username="lilian")['access']
|
||||||
token2 = test_register(client, username="lilian2")['access']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
|
|
||||||
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv = test_create(client, private=True, user={
|
||||||
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv2 = test_create(client, private=True, user={
|
||||||
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv3 = test_create(client, private=True, user={
|
||||||
prv6= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv7= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv4 = test_create(client, private=True, user={
|
||||||
prv8= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv9= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv5 = test_create(client, private=True, user={
|
||||||
prv10= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
prv11 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
prv6 = test_create(client, private=True, user={
|
||||||
prv12 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv7 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv8 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv9 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv10 = test_create(client, private=True, user={
|
||||||
|
'token': token1, 'username': "lilian"})
|
||||||
|
prv11 = test_create(client, private=True, user={
|
||||||
|
'token': token2, 'username': "lilian2"})
|
||||||
|
prv12 = test_create(client, private=True, user={
|
||||||
|
'token': token2, 'username': "lilian2"})
|
||||||
|
|
||||||
r = client.get('/exercices/user',
|
r = client.get('/exercices/user',
|
||||||
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
|
headers={'Authorization': 'Bearer ' + token1}, params={"page": 2, "size": 10})
|
||||||
@ -393,6 +427,7 @@ def test_get_users_exos_page_up(client: TestClient):
|
|||||||
assert r.json()["items"] == []
|
assert r.json()["items"] == []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_user_with_search(client: TestClient):
|
def test_get_user_with_search(client: TestClient):
|
||||||
token1 = test_register(client, username="lilian")['access']
|
token1 = test_register(client, username="lilian")['access']
|
||||||
token2 = test_register(client, username="lilian2")['access']
|
token2 = test_register(client, username="lilian2")['access']
|
||||||
@ -553,7 +588,7 @@ def test_get_public_no_auth(client: TestClient):
|
|||||||
r = client.get('/exercices/public')
|
r = client.get('/exercices/public')
|
||||||
print(r.json())
|
print(r.json())
|
||||||
assert r.json()['items'] == [{**public1, 'is_author': False},
|
assert r.json()['items'] == [{**public1, 'is_author': False},
|
||||||
{**public2, 'is_author': False}]
|
{**public2, 'is_author': False}]
|
||||||
|
|
||||||
|
|
||||||
def test_get_exo_no_auth(client: TestClient):
|
def test_get_exo_no_auth(client: TestClient):
|
||||||
@ -561,7 +596,7 @@ def test_get_exo_no_auth(client: TestClient):
|
|||||||
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
exo = test_add_tags(client, user={'token': token, 'username': "lilian"})
|
||||||
|
|
||||||
r = client.get('/exercice/' + exo['id_code'])
|
r = client.get('/exercice/' + exo['id_code'])
|
||||||
assert r.json()['items'] == {**exo, "tags": [], 'is_author': False}
|
assert r.json() == {**exo, "tags": [], 'is_author': False}
|
||||||
|
|
||||||
|
|
||||||
def test_get_exo_no_auth_private(client: TestClient):
|
def test_get_exo_no_auth_private(client: TestClient):
|
||||||
@ -630,7 +665,7 @@ def test_get_csv(client: TestClient):
|
|||||||
|
|
||||||
r = client.get('/exercices/public', params={"type": "csv"})
|
r = client.get('/exercices/public', params={"type": "csv"})
|
||||||
|
|
||||||
assert r.json() == [{**exoCsv.json(), 'is_author': False}]
|
assert r.json()['items'] == [{**exoCsv.json(), 'is_author': False}]
|
||||||
|
|
||||||
|
|
||||||
def test_get_pdf(client: TestClient):
|
def test_get_pdf(client: TestClient):
|
||||||
@ -644,7 +679,7 @@ def test_get_pdf(client: TestClient):
|
|||||||
|
|
||||||
r = client.get('/exercices/public', params={"type": "pdf"})
|
r = client.get('/exercices/public', params={"type": "pdf"})
|
||||||
|
|
||||||
assert r.json() == [{**exoPdf.json(), 'is_author': False}]
|
assert r.json()['items'] == [{**exoPdf.json(), 'is_author': False}]
|
||||||
|
|
||||||
|
|
||||||
def test_get_web(client: TestClient):
|
def test_get_web(client: TestClient):
|
||||||
@ -657,12 +692,13 @@ def test_get_web(client: TestClient):
|
|||||||
'file': ('test.py', open('tests/testing_exo_source/exo_source_web_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
'file': ('test.py', open('tests/testing_exo_source/exo_source_web_only.py', 'rb'))}, headers={"Authorization": "Bearer " + token})
|
||||||
|
|
||||||
r = client.get('/exercices/public', params={"type": "web"})
|
r = client.get('/exercices/public', params={"type": "web"})
|
||||||
|
|
||||||
assert r.json() == [{**exoWeb.json(), 'is_author': False}]
|
assert r.json() == [{**exoWeb.json(), 'is_author': False}]
|
||||||
|
|
||||||
|
|
||||||
def test_get_invalid_type(client: TestClient):
|
def test_get_invalid_type(client: TestClient):
|
||||||
|
|
||||||
r = client.get('/exercices/public', params={"type": "lol"})
|
r = client.get('/exercices/public', params={"type": "lol"})
|
||||||
|
|
||||||
assert r.json() == {"detail": {'type_error': "value is not a valid enumeration member; permitted: 'csv', 'pdf', 'web'"}}
|
assert r.json() == {"detail": {
|
||||||
|
'type_error': "value is not a valid enumeration member; permitted: 'csv', 'pdf', 'web'"}}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
"@sveltejs/kit": "^1.0.0",
|
"@sveltejs/kit": "^1.0.0",
|
||||||
|
"@types/chroma-js": "^2.1.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
@ -21,13 +22,27 @@
|
|||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-svelte": "^2.8.1",
|
"prettier-plugin-svelte": "^2.8.1",
|
||||||
|
"sass": "^1.53.0",
|
||||||
"svelte": "^3.54.0",
|
"svelte": "^3.54.0",
|
||||||
"svelte-check": "^2.9.2",
|
"svelte-check": "^2.9.2",
|
||||||
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
"vite": "^4.0.0",
|
"vite": "^4.0.0"
|
||||||
"sass": "^1.53.0",
|
|
||||||
"svelte-preprocess": "^4.10.7"
|
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@sveltestack/svelte-query": "^1.6.0",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
|
"axios": "^1.2.2",
|
||||||
|
"chroma-js": "^2.4.2",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"qs": "^6.11.0",
|
||||||
|
"svelecte": "^3.13.0",
|
||||||
|
"svelte-forms": "^2.3.1",
|
||||||
|
"svelte-icons": "^2.1.0",
|
||||||
|
"svelte-multiselect": "^8.2.3",
|
||||||
|
"svelte-navigator": "^3.2.2",
|
||||||
|
"svelte-routing": "^1.6.0"
|
||||||
|
}
|
||||||
}
|
}
|
244
frontend/pnpm-lock.yaml
generated
244
frontend/pnpm-lock.yaml
generated
@ -3,24 +3,52 @@ lockfileVersion: 5.4
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@sveltejs/adapter-auto': ^1.0.0
|
'@sveltejs/adapter-auto': ^1.0.0
|
||||||
'@sveltejs/kit': ^1.0.0
|
'@sveltejs/kit': ^1.0.0
|
||||||
|
'@sveltestack/svelte-query': ^1.6.0
|
||||||
|
'@types/chroma-js': ^2.1.4
|
||||||
|
'@types/qs': ^6.9.7
|
||||||
'@typescript-eslint/eslint-plugin': ^5.45.0
|
'@typescript-eslint/eslint-plugin': ^5.45.0
|
||||||
'@typescript-eslint/parser': ^5.45.0
|
'@typescript-eslint/parser': ^5.45.0
|
||||||
|
axios: ^1.2.2
|
||||||
|
chroma-js: ^2.4.2
|
||||||
eslint: ^8.28.0
|
eslint: ^8.28.0
|
||||||
eslint-config-prettier: ^8.5.0
|
eslint-config-prettier: ^8.5.0
|
||||||
eslint-plugin-svelte3: ^4.0.0
|
eslint-plugin-svelte3: ^4.0.0
|
||||||
|
jwt-decode: ^3.1.2
|
||||||
prettier: ^2.8.0
|
prettier: ^2.8.0
|
||||||
prettier-plugin-svelte: ^2.8.1
|
prettier-plugin-svelte: ^2.8.1
|
||||||
|
qs: ^6.11.0
|
||||||
sass: ^1.53.0
|
sass: ^1.53.0
|
||||||
|
svelecte: ^3.13.0
|
||||||
svelte: ^3.54.0
|
svelte: ^3.54.0
|
||||||
svelte-check: ^2.9.2
|
svelte-check: ^2.9.2
|
||||||
|
svelte-forms: ^2.3.1
|
||||||
|
svelte-icons: ^2.1.0
|
||||||
|
svelte-multiselect: ^8.2.3
|
||||||
|
svelte-navigator: ^3.2.2
|
||||||
svelte-preprocess: ^4.10.7
|
svelte-preprocess: ^4.10.7
|
||||||
|
svelte-routing: ^1.6.0
|
||||||
tslib: ^2.4.1
|
tslib: ^2.4.1
|
||||||
typescript: ^4.9.3
|
typescript: ^4.9.3
|
||||||
vite: ^4.0.0
|
vite: ^4.0.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
'@sveltestack/svelte-query': 1.6.0
|
||||||
|
'@types/qs': 6.9.7
|
||||||
|
axios: 1.2.2
|
||||||
|
chroma-js: 2.4.2
|
||||||
|
jwt-decode: 3.1.2
|
||||||
|
qs: 6.11.0
|
||||||
|
svelecte: 3.13.0
|
||||||
|
svelte-forms: 2.3.1
|
||||||
|
svelte-icons: 2.1.0
|
||||||
|
svelte-multiselect: 8.2.3
|
||||||
|
svelte-navigator: 3.2.2_niwyv7xychq2ag6arq5eqxbomm
|
||||||
|
svelte-routing: 1.6.0_niwyv7xychq2ag6arq5eqxbomm
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1
|
'@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1
|
||||||
'@sveltejs/kit': 1.0.1_svelte@3.55.0+vite@4.0.3
|
'@sveltejs/kit': 1.0.1_svelte@3.55.0+vite@4.0.3
|
||||||
|
'@types/chroma-js': 2.1.4
|
||||||
'@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy
|
'@typescript-eslint/eslint-plugin': 5.47.1_txmweb6yn7coi7nfrp22gpyqmy
|
||||||
'@typescript-eslint/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
|
'@typescript-eslint/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
|
||||||
eslint: 8.30.0
|
eslint: 8.30.0
|
||||||
@ -370,6 +398,19 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@sveltestack/svelte-query/1.6.0:
|
||||||
|
resolution: {integrity: sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==}
|
||||||
|
peerDependencies:
|
||||||
|
broadcast-channel: ^4.5.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
broadcast-channel:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/chroma-js/2.1.4:
|
||||||
|
resolution: {integrity: sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/cookie/0.5.1:
|
/@types/cookie/0.5.1:
|
||||||
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
|
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -386,6 +427,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/qs/6.9.7:
|
||||||
|
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/sass/1.43.1:
|
/@types/sass/1.43.1:
|
||||||
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
|
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -577,6 +622,20 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/asynckit/0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/axios/1.2.2:
|
||||||
|
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.2
|
||||||
|
form-data: 4.0.0
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
/balanced-match/1.0.2:
|
/balanced-match/1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -611,6 +670,13 @@ packages:
|
|||||||
streamsearch: 1.1.0
|
streamsearch: 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/call-bind/1.0.2:
|
||||||
|
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.1
|
||||||
|
get-intrinsic: 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/callsites/3.1.0:
|
/callsites/3.1.0:
|
||||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -639,6 +705,10 @@ packages:
|
|||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/chroma-js/2.4.2:
|
||||||
|
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/color-convert/2.0.1:
|
/color-convert/2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@ -650,6 +720,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/combined-stream/1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/concat-map/0.0.1:
|
/concat-map/0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -680,6 +757,10 @@ packages:
|
|||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/dedent-js/1.0.1:
|
||||||
|
resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/deep-is/0.1.4:
|
/deep-is/0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -689,6 +770,11 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/delayed-stream/1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/detect-indent/6.1.0:
|
/detect-indent/6.1.0:
|
||||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -959,6 +1045,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/follow-redirects/1.15.2:
|
||||||
|
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/form-data/4.0.0:
|
||||||
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: false
|
||||||
|
|
||||||
/fs.realpath/1.0.0:
|
/fs.realpath/1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -973,7 +1078,14 @@ packages:
|
|||||||
|
|
||||||
/function-bind/1.1.1:
|
/function-bind/1.1.1:
|
||||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||||
dev: true
|
|
||||||
|
/get-intrinsic/1.1.3:
|
||||||
|
resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.1
|
||||||
|
has: 1.0.3
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/glob-parent/5.1.2:
|
/glob-parent/5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
@ -1040,12 +1152,16 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/has-symbols/1.0.3:
|
||||||
|
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/has/1.0.3:
|
/has/1.0.3:
|
||||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ignore/5.2.4:
|
/ignore/5.2.4:
|
||||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||||
@ -1119,6 +1235,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-promise/4.0.0:
|
||||||
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/isexe/2.0.0:
|
/isexe/2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1142,6 +1262,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jwt-decode/3.1.2:
|
||||||
|
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/kleur/4.1.5:
|
/kleur/4.1.5:
|
||||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1166,6 +1290,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lower-case/2.0.2:
|
||||||
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lru-cache/6.0.0:
|
/lru-cache/6.0.0:
|
||||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -1199,6 +1329,18 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mime-db/1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mime-types/2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mime/3.0.0:
|
/mime/3.0.0:
|
||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@ -1255,11 +1397,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/no-case/3.0.4:
|
||||||
|
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||||
|
dependencies:
|
||||||
|
lower-case: 2.0.2
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/normalize-path/3.0.0:
|
/normalize-path/3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/object-inspect/1.12.2:
|
||||||
|
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/once/1.4.0:
|
/once/1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1299,6 +1452,13 @@ packages:
|
|||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pascal-case/3.1.2:
|
||||||
|
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/path-exists/4.0.0:
|
/path-exists/4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1362,11 +1522,22 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/proxy-from-env/1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/punycode/2.1.1:
|
/punycode/2.1.1:
|
||||||
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/qs/6.11.0:
|
||||||
|
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dependencies:
|
||||||
|
side-channel: 1.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/queue-microtask/1.2.3:
|
/queue-microtask/1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1480,6 +1651,14 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/side-channel/1.0.4:
|
||||||
|
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.2
|
||||||
|
get-intrinsic: 1.1.3
|
||||||
|
object-inspect: 1.12.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sirv/2.0.2:
|
/sirv/2.0.2:
|
||||||
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
|
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@ -1550,6 +1729,12 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelecte/3.13.0:
|
||||||
|
resolution: {integrity: sha512-PwAV9+45+fVJsWFiM+xX+82qKs+GuL1hSUIajnEMMjbomLgoT6b0Z4dcyIRSjtCJrUtbBVQF6UG2Ekx4HFldNA==}
|
||||||
|
dependencies:
|
||||||
|
svelte-tiny-virtual-list: 2.0.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-check/2.10.3_sass@1.57.1+svelte@3.55.0:
|
/svelte-check/2.10.3_sass@1.57.1+svelte@3.55.0:
|
||||||
resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==}
|
resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -1578,6 +1763,12 @@ packages:
|
|||||||
- sugarss
|
- sugarss
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-forms/2.3.1:
|
||||||
|
resolution: {integrity: sha512-ExX9PM0JgvdOWlHl2ztD7XzLNPOPt9U5hBKV8sUAisMfcYWpPRnyz+6EFmh35BOBGJJmuhTDBGm5/7seLjOTIA==}
|
||||||
|
dependencies:
|
||||||
|
is-promise: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-hmr/0.15.1_svelte@3.55.0:
|
/svelte-hmr/0.15.1_svelte@3.55.0:
|
||||||
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
||||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||||
@ -1587,6 +1778,25 @@ packages:
|
|||||||
svelte: 3.55.0
|
svelte: 3.55.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-icons/2.1.0:
|
||||||
|
resolution: {integrity: sha512-rHPQjweEc9fGSnvM0/4gA3pDHwyZyYsC5KhttCZRhSMJfLttJST5Uq0B16Czhw+HQ+HbSOk8kLigMlPs7gZtfg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/svelte-multiselect/8.2.3:
|
||||||
|
resolution: {integrity: sha512-cCnPFkG+0i2eBDaYUOgmQVa2TaJ6Xdjly0/tpch0XCfu4Rs0whbnEXP4QfKVloaAxEDUXwiIq/FHEYZ61xAklg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/svelte-navigator/3.2.2_niwyv7xychq2ag6arq5eqxbomm:
|
||||||
|
resolution: {integrity: sha512-Xio4ohLUG1nQJ+ENNbLphXXu9L189fnI1WGg+2Q3CIMPe8Jm2ipytKQthdBs8t0mN7p3Eb03SE9hq0xZAqwQNQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: 3.x
|
||||||
|
dependencies:
|
||||||
|
svelte: 3.55.0
|
||||||
|
svelte2tsx: 0.1.193_niwyv7xychq2ag6arq5eqxbomm
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte-preprocess/4.10.7_cfliyikhlimajcn5n7qvd3jsli:
|
/svelte-preprocess/4.10.7_cfliyikhlimajcn5n7qvd3jsli:
|
||||||
resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==}
|
resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==}
|
||||||
engines: {node: '>= 9.11.2'}
|
engines: {node: '>= 9.11.2'}
|
||||||
@ -1639,10 +1849,36 @@ packages:
|
|||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/svelte-routing/1.6.0_niwyv7xychq2ag6arq5eqxbomm:
|
||||||
|
resolution: {integrity: sha512-+DbrSGttLA6lan7oWFz1MjyGabdn3tPRqn8Osyc471ut2UgCrzM5x1qViNMc2gahOP6fKbKK1aNtZMJEQP2vHQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3.20.x
|
||||||
|
dependencies:
|
||||||
|
svelte: 3.55.0
|
||||||
|
svelte2tsx: 0.1.193_niwyv7xychq2ag6arq5eqxbomm
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- typescript
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/svelte-tiny-virtual-list/2.0.5:
|
||||||
|
resolution: {integrity: sha512-xg9ckb8UeeIme4/5qlwCrl2QNmUZ8SCQYZn3Ji83cUsoASqRNy3KWjpmNmzYvPDqCHSZjruBBsoB7t5hwuzw5g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/svelte/3.55.0:
|
/svelte/3.55.0:
|
||||||
resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==}
|
resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
dev: true
|
|
||||||
|
/svelte2tsx/0.1.193_niwyv7xychq2ag6arq5eqxbomm:
|
||||||
|
resolution: {integrity: sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3.24
|
||||||
|
typescript: ^4.1.2
|
||||||
|
dependencies:
|
||||||
|
dedent-js: 1.0.1
|
||||||
|
pascal-case: 3.1.2
|
||||||
|
svelte: 3.55.0
|
||||||
|
typescript: 4.9.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/text-table/0.2.0:
|
/text-table/0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
@ -1673,7 +1909,6 @@ packages:
|
|||||||
|
|
||||||
/tslib/2.4.1:
|
/tslib/2.4.1:
|
||||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/tsutils/3.21.0_typescript@4.9.4:
|
/tsutils/3.21.0_typescript@4.9.4:
|
||||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||||
@ -1701,7 +1936,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
|
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
|
||||||
engines: {node: '>=4.2.0'}
|
engines: {node: '>=4.2.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/undici/5.14.0:
|
/undici/5.14.0:
|
||||||
resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==}
|
resolution: {integrity: sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==}
|
||||||
|
15
frontend/src/apis/exo.api.ts
Normal file
15
frontend/src/apis/exo.api.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { parse, stringify } from 'qs'
|
||||||
|
export const exoInstance = axios.create({
|
||||||
|
paramsSerializer:{encode:(params)=> {return parse(params, {arrayFormat:"brackets"})}, serialize: (p)=>{return stringify(p, {arrayFormat: "repeat"})}},
|
||||||
|
baseURL: `http://127.0.0.1:8002`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
//'X-CSRFToken': csrftoken != undefined ? csrftoken : '',
|
||||||
|
...(browser &&
|
||||||
|
localStorage.getItem('token') && { Authorization: `Bearer ${localStorage.getItem('token')}` })
|
||||||
|
}
|
||||||
|
});
|
@ -1,4 +1,82 @@
|
|||||||
/* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */
|
/* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */
|
||||||
.input{
|
|
||||||
background-color: red;
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 38px;
|
||||||
|
font-weight: 700;
|
||||||
|
transition: 0.3s;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-right: 7px;
|
||||||
|
padding: 0 10%;
|
||||||
|
width: max-content;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
@extend .btn;
|
||||||
|
background-color: #fcbf49;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ac7b19;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-btn {
|
||||||
|
@extend .btn;
|
||||||
|
background-color: #fc5e49;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ac1919;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary-btn {
|
||||||
|
@extend .btn;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #fcbf49;
|
||||||
|
color: #fcbf49;
|
||||||
|
&:hover {
|
||||||
|
background-color: #fcbf49;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: inherit;
|
||||||
|
color: inherit;
|
||||||
|
padding: 5px 10px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 450;
|
||||||
|
margin: 10px 0;
|
||||||
|
float: left;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #181553;
|
||||||
|
transition: 0.3s;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-bottom-color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flex-row-center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
@for $f from 0 through 100 {
|
||||||
|
.wp-#{$f} {
|
||||||
|
width: 1% * $f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sv-dropdown{
|
||||||
|
z-index: 10!important;
|
||||||
}
|
}
|
@ -1,49 +1,143 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
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 { cloneExo } from '../../requests/exo.request';
|
||||||
|
import TagContainer from './TagContainer.svelte';
|
||||||
|
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||||
|
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||||
|
export let exo: Exercice;
|
||||||
|
|
||||||
export let title: string;
|
const { show } = getContext<{ show: Function }>('modal');
|
||||||
export let examples: Array<string>;
|
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||||
export let tags: Array<string>;
|
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||||
|
const exerciceStore = getContext('exos');
|
||||||
const { show } = getContext<{show: Function}>('modal');
|
const tagsStore = getContext('tags');
|
||||||
|
|
||||||
|
let opened = false;
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
console.log('OOOPPP');
|
opened = true;
|
||||||
show(ModalCard, {exo: {title, examples, tags}})
|
navigate(`/exercices/${exo.id_code}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tg = false;
|
||||||
|
$: !!opened &&
|
||||||
|
show(
|
||||||
|
ModalCard,
|
||||||
|
{
|
||||||
|
exo,
|
||||||
|
exos: exerciceStore,
|
||||||
|
tags: tagsStore
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
navigate(-1);
|
||||||
|
opened = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card" on:click={handleClick}>
|
<div
|
||||||
<h1>{title}</h1>
|
class="card"
|
||||||
|
class:tagMode={tg}
|
||||||
|
on:click={handleClick}
|
||||||
|
on:dblclick={() => {}}
|
||||||
|
on:keypress={() => {}}
|
||||||
|
>
|
||||||
|
<h1>{exo.name}</h1>
|
||||||
<div class="examples">
|
<div class="examples">
|
||||||
<h2>Exemples</h2>
|
<h2>Exemples</h2>
|
||||||
{#each examples.slice(0, 3) as ex}
|
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
|
||||||
<p>{ex}</p>
|
{#each exo.examples.data.slice(0, 3) as ex}
|
||||||
|
<p>{ex.calcul}</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div
|
{#if !!isAuth}
|
||||||
class="tags"
|
{#if exo.is_author && exo.original == null}
|
||||||
on:click={() => {
|
<div class="status">
|
||||||
alert('test');
|
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||||
}}
|
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||||
/>
|
>
|
||||||
|
</div>
|
||||||
|
{:else if !exo.is_author}
|
||||||
|
<div class="status">
|
||||||
|
<PrivacyIndicator color={'blue'}>
|
||||||
|
Par <strong>{exo.author.username}</strong>
|
||||||
|
</PrivacyIndicator>
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:keydown={() => {}}
|
||||||
|
on:click|stopPropagation={() => {
|
||||||
|
cloneExo(exo.id_code).then((r) => {
|
||||||
|
goto('/exercices/' + r.id_code);
|
||||||
|
show(ModalCard, { exo: r }, () => {
|
||||||
|
goto('/exercices/user');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdContentCopy />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if exo.is_author && exo.original != null}
|
||||||
|
<div class="status">
|
||||||
|
<PrivacyIndicator color="blue">Par <strong>{exo.original?.author}</strong></PrivacyIndicator
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}{/if}
|
||||||
<div class="card-hover" />
|
<div class="card-hover" />
|
||||||
|
{#if !!isAuth}
|
||||||
|
<TagContainer bind:exo />
|
||||||
|
{/if}
|
||||||
|
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
* {
|
* {
|
||||||
transition: 0.45s;
|
transition: 0.45s;
|
||||||
}
|
}
|
||||||
|
.icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(0.9);
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.examples {
|
.examples {
|
||||||
color: gray;
|
color: gray;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-bottom: 20px;
|
||||||
p {
|
p {
|
||||||
|
margin: 10px;
|
||||||
margin-left: 18px;
|
margin-left: 18px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
margin: 10px;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +148,16 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border: 1px solid green;
|
border: 1px solid $border;
|
||||||
|
+ :global(div) {
|
||||||
|
transition: 0.45s;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid red;
|
border: 1px solid $primary;
|
||||||
|
+ :global(div) {
|
||||||
|
border: 1px solid $primary;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
|
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.75);
|
||||||
}
|
}
|
||||||
@ -66,19 +167,17 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
&:hover {
|
max-width: 88%;
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
overflow: hidden;
|
||||||
height: 20px;
|
text-overflow: ellipsis;
|
||||||
position: absolute;
|
display: -webkit-box;
|
||||||
bottom: 1px;
|
-webkit-line-clamp: 2;
|
||||||
background-color: blue;
|
-webkit-box-orient: vertical;
|
||||||
right: 1px;
|
word-wrap: break-word;
|
||||||
left: 1px;
|
&:hover {
|
||||||
z-index: 3;
|
color: $primary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@ -86,9 +185,10 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: violet;
|
background-color: $background;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
&:hover {
|
max-height: 300px;
|
||||||
|
&:not(.tagMode):hover {
|
||||||
transform: translateX(10px) translateY(-10px);
|
transform: translateX(10px) translateY(-10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
frontend/src/components/exos/CreateCard.svelte
Normal file
32
frontend/src/components/exos/CreateCard.svelte
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Exercice, Page } from '../../types/exo.type';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import EditForm from './EditForm.svelte';
|
||||||
|
|
||||||
|
export let cancel: Function;
|
||||||
|
export let exos: Writable<{ isLoading: boolean; isFetching: boolean; data: Page }>;
|
||||||
|
const updateExo = (e: Exercice) => {
|
||||||
|
exos.update((o) => {
|
||||||
|
return { ...o, data: { ...o.data, items: [e, ...o.data.items] } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1>Nouvel exercice</h1>
|
||||||
|
<EditForm editing={false} {cancel} {updateExo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background-color: blue;
|
||||||
|
padding: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
199
frontend/src/components/exos/DownloadForm.svelte
Normal file
199
frontend/src/components/exos/DownloadForm.svelte
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MdClose from 'svelte-icons/md/MdClose.svelte';
|
||||||
|
import MdEdit from 'svelte-icons/md/MdEdit.svelte';
|
||||||
|
import MdDelete from 'svelte-icons/md/MdDelete.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import InputWithLabel from '../forms/InputWithLabel.svelte';
|
||||||
|
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
|
||||||
|
import { cloneExo, delExo, getExo } from '../../requests/exo.request';
|
||||||
|
import type { Exercice } from 'src/types/exo.type';
|
||||||
|
import TagContainer from './TagContainer.svelte';
|
||||||
|
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||||
|
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||||
|
import ModalCard from './ModalCard.svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
export let exo: Exercice;
|
||||||
|
export let edit: Function;
|
||||||
|
export let delete_: Function;
|
||||||
|
|
||||||
|
const { close, show } = getContext<{ close: Function; show: Function }>('modal');
|
||||||
|
const { alert } = getContext<{ alert: Function }>('alert');
|
||||||
|
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if exo != null}
|
||||||
|
<h1>
|
||||||
|
<span class="name">{exo.name}</span>
|
||||||
|
{#if exo.is_author && exo.original == null}
|
||||||
|
<span
|
||||||
|
><PrivacyIndicator color={exo.private == true ? 'red' : 'green'}
|
||||||
|
>{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
{:else if exo.is_author && exo.original != null}
|
||||||
|
<span
|
||||||
|
on:click={() => {
|
||||||
|
if (exo.original == null) return;
|
||||||
|
|
||||||
|
getExo(exo.original?.id_code).then((r) => {
|
||||||
|
goto(`/exercices/${exo.original?.id_code}`);
|
||||||
|
show(
|
||||||
|
ModalCard,
|
||||||
|
{ exo: r },
|
||||||
|
() => {
|
||||||
|
goto('/exercices/user');
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
on:keyup={() => {}}
|
||||||
|
>
|
||||||
|
<PrivacyIndicator color="blue"
|
||||||
|
>Exercice original de <strong>{exo.original?.author}</strong></PrivacyIndicator
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
{:else if !exo.is_author && exo.original == null}
|
||||||
|
<span>
|
||||||
|
<PrivacyIndicator color="blue">Par <strong>{exo.author.username}</strong></PrivacyIndicator>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
<InputWithLabel type="text" value={name} label="Nom" />
|
||||||
|
|
||||||
|
<div class="examples">
|
||||||
|
<h2>Exemples</h2>
|
||||||
|
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
|
||||||
|
|
||||||
|
{#each exo.examples.data as e}
|
||||||
|
<p>{e.calcul}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="flex-row-center wp-100">
|
||||||
|
<button class="primary-btn">Télécharger</button>
|
||||||
|
</div>
|
||||||
|
<div class="tags" />
|
||||||
|
|
||||||
|
{#if !!isAuth}
|
||||||
|
<TagContainer {exo} />
|
||||||
|
{/if}
|
||||||
|
<div class="icons">
|
||||||
|
<div>
|
||||||
|
<div class="icon" style:color="black" on:click={() => close()} on:keypress={() => {}}>
|
||||||
|
<MdClose />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if !!isAuth}
|
||||||
|
{#if exo.is_author}
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
style:color="red"
|
||||||
|
on:click={() => {
|
||||||
|
alert({
|
||||||
|
title: 'Sur ?',
|
||||||
|
description: 'Voulez vous supprimer ? ',
|
||||||
|
validate: () => {
|
||||||
|
close();
|
||||||
|
delete_();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
on:keypress={() => {}}
|
||||||
|
>
|
||||||
|
<MdDelete />
|
||||||
|
</div>
|
||||||
|
<div class="icon" style:color="green" on:click={() => edit()} on:keypress={() => {}}>
|
||||||
|
<MdEdit />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
style:color="#f0f0f0"
|
||||||
|
on:click={() => {
|
||||||
|
cloneExo(exo.id_code).then((r) => {
|
||||||
|
goto('/exercices/' + r.id_code);
|
||||||
|
show(
|
||||||
|
ModalCard,
|
||||||
|
{ exo: r },
|
||||||
|
() => {
|
||||||
|
goto('/exercices/user');
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
on:keypress={() => {}}
|
||||||
|
title="Copier l'exercice pour pouvoir le modifier"
|
||||||
|
>
|
||||||
|
<MdContentCopy />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(0.9);
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
margin: 30px;
|
||||||
|
gap: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
span.name {
|
||||||
|
overflow: hidden;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
}
|
||||||
|
span:not(.name) {
|
||||||
|
position: relative;
|
||||||
|
height: min-content;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples {
|
||||||
|
h2 {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 5px 0;
|
||||||
|
margin-left: 30px;
|
||||||
|
color: gray;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
118
frontend/src/components/exos/EditForm.svelte
Normal file
118
frontend/src/components/exos/EditForm.svelte
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { form, field } from 'svelte-forms';
|
||||||
|
import { required, max, min } from 'svelte-forms/validators';
|
||||||
|
import FileInput from '../forms/FileInput.svelte';
|
||||||
|
import InputWithLabel from '../forms/InputWithLabel.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { createExo, editExo } from '../../requests/exo.request';
|
||||||
|
import type { Exercice } from '../../types/exo.type';
|
||||||
|
import { checkFile, errorMsg } from '../../utils/forms';
|
||||||
|
import { compareObject } from '../../utils/utils';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
export let editing = true;
|
||||||
|
export let updateExo: Function = (e: Exercice) => {};
|
||||||
|
|
||||||
|
export let exo: Exercice | null = null;
|
||||||
|
export let cancel: Function;
|
||||||
|
|
||||||
|
const { alert } = getContext<{ alert: Function }>('alert');
|
||||||
|
|
||||||
|
// "Legally" initiate empty FileList for model field (simple list raises warning)
|
||||||
|
let list = new DataTransfer();
|
||||||
|
let file = new File(['content'], !editing || exo == null ? 'filename.py' : exo.exo_source);
|
||||||
|
list.items.add(file);
|
||||||
|
!editing && list.items.remove(0);
|
||||||
|
|
||||||
|
// Initiate fields and form
|
||||||
|
const name = field('name', !!exo ? exo.name : '', [required(), max(50), min(5)], {
|
||||||
|
checkOnInit: true
|
||||||
|
});
|
||||||
|
const consigne = field('consigne', !!exo && exo.consigne != null ? exo.consigne : '', [max(200)], { checkOnInit: true });
|
||||||
|
const prv = field('private', !!exo ? exo.private : false);
|
||||||
|
const model = field('model', list.files, [checkFile(), required()], {
|
||||||
|
checkOnInit: !editing
|
||||||
|
});
|
||||||
|
const myForm = form(name, consigne, prv, model);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form
|
||||||
|
action=""
|
||||||
|
on:submit|preventDefault={() => {
|
||||||
|
if (editing && exo != null) {
|
||||||
|
editExo(exo.id_code, {
|
||||||
|
name: $name.value,
|
||||||
|
consigne: $consigne.value,
|
||||||
|
private: $prv.value,
|
||||||
|
...($model.dirty == true && { file: $model.value[0] })
|
||||||
|
}).then((r) => {
|
||||||
|
exo=r.data
|
||||||
|
updateExo(r.data);
|
||||||
|
cancel()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createExo({
|
||||||
|
name: $name.value,
|
||||||
|
consigne: $consigne.value,
|
||||||
|
private: $prv.value,
|
||||||
|
file: $model.value[0]
|
||||||
|
}).then((r) => {
|
||||||
|
updateExo(r.data);
|
||||||
|
cancel()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputWithLabel
|
||||||
|
type="text"
|
||||||
|
bind:value={$name.value}
|
||||||
|
maxlength="50"
|
||||||
|
minlength="5"
|
||||||
|
required
|
||||||
|
label="Nom"
|
||||||
|
errors={errorMsg($myForm, 'name')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputWithLabel
|
||||||
|
type="text"
|
||||||
|
bind:value={$consigne.value}
|
||||||
|
maxlength="200"
|
||||||
|
label="Consigne"
|
||||||
|
errors={errorMsg($myForm, 'consigne')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" bind:checked={$prv.value} name="private" id="private" />
|
||||||
|
<label for="private">Privé</label>
|
||||||
|
</div>
|
||||||
|
<FileInput bind:value={$model.value} accept=".py" id_code={exo?.id_code} />
|
||||||
|
|
||||||
|
<div class="wp-100">
|
||||||
|
<button class="primary-btn" disabled={!$myForm.valid}>Valider</button>
|
||||||
|
<button
|
||||||
|
class="danger-btn"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
|
||||||
|
if (exo != null && ($model.dirty || !compareObject({...exo, consigne: exo.consigne == null ? "": exo.consigne}, myForm.summary()))) {
|
||||||
|
alert({
|
||||||
|
title: 'test',
|
||||||
|
description:
|
||||||
|
'Aliquip in cupidatat anim tempor quis est sint qui sunt. Magna consequat excepteur deserunt ullamco quis.',
|
||||||
|
validate: cancel
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
}}>Annuler</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,31 +1,89 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { each } from 'svelte/internal';
|
import { getContext } from 'svelte/internal';
|
||||||
import Card from './Card.svelte';
|
import Card from './Card.svelte';
|
||||||
|
import Head from './Head.svelte';
|
||||||
import ModalCard from './ModalCard.svelte';
|
import ModalCard from './ModalCard.svelte';
|
||||||
let exos = [
|
import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
|
||||||
{
|
import { getExo, getExos, getTags } from '../../requests/exo.request';
|
||||||
title: 'test1',
|
import { page } from '$app/stores';
|
||||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
import { onMount } from 'svelte';
|
||||||
tags: []
|
import Pagination from './Pagination.svelte';
|
||||||
},
|
import { goto } from '$app/navigation';
|
||||||
{
|
import { writable } from 'svelte/store';
|
||||||
title: 'test2',
|
import { setContext } from 'svelte';
|
||||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
import type { Page, Tag } from '../../types/exo.type';
|
||||||
tags: []
|
import type { Store } from '../../types/api.type';
|
||||||
},
|
|
||||||
{
|
const { show } = getContext<{ show: Function }>('modal');
|
||||||
title: 'test3',
|
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
|
||||||
tags: []
|
let filter = 'user';
|
||||||
},
|
|
||||||
{
|
const exerciceStore = writable<Store<Page|undefined>>({
|
||||||
title: 'test1',
|
isLoading: false,
|
||||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
isFetching: false,
|
||||||
tags: []
|
isSuccess: false,
|
||||||
|
data: undefined
|
||||||
|
});
|
||||||
|
const tagStore = writable<Store<Tag[]|undefined>>({
|
||||||
|
isLoading: false,
|
||||||
|
isFetching: false,
|
||||||
|
isSuccess: false,
|
||||||
|
data: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
setContext('exos', exerciceStore);
|
||||||
|
setContext('tags', tagStore);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if ($page.params.slug != undefined && !['user', 'public'].includes($page.params.slug)) {
|
||||||
|
getExo($page.params.slug).then((r) => {
|
||||||
|
show(ModalCard, { exo: r, exos: exerciceStore, tags: tagStore }, () => navigate('/exercices/' + filter));
|
||||||
|
});
|
||||||
|
} else if ($page.params.slug == undefined) {
|
||||||
|
goto('/exercices/public');
|
||||||
}
|
}
|
||||||
];
|
});
|
||||||
|
|
||||||
|
$: filter = ['user', 'public'].includes($page.params.slug) ? $page.params.slug : filter;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
exerciceStore.update((s) => {
|
||||||
|
return { ...s, isFetching: true };
|
||||||
|
});
|
||||||
|
getExos(filter as 'public' | 'user', {
|
||||||
|
page: activePage,
|
||||||
|
search,
|
||||||
|
tags: [...selected.map((t) => t.id_code)]
|
||||||
|
}).then((r) => {
|
||||||
|
exerciceStore.update((e) => {
|
||||||
|
return { ...e, isSuccess: true, isFetching: false, data: r };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
tagStore.update((s)=>{return {...s, isFetching: true}});
|
||||||
|
getTags().then(r=>{
|
||||||
|
tagStore.update((e) => {
|
||||||
|
return { ...e, isSuccess: true, isFetching: false, data: r };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let activePage = parseInt($page.url.searchParams.get('page')!) || 1;
|
||||||
|
let search = '';
|
||||||
|
let selected: Tag[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#if $tagStore.isSuccess == true && $tagStore.data != undefined}
|
||||||
|
<Head location={filter} bind:search bind:selected />
|
||||||
|
{/if}
|
||||||
|
{#if $tagStore.isFetching == true}
|
||||||
|
Fetching
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="feed">
|
<div class="feed">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>
|
<h1>
|
||||||
@ -36,9 +94,14 @@
|
|||||||
publics
|
publics
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#each exos as e}
|
{#if $exerciceStore.data != undefined}
|
||||||
<Card examples={e.examples} title={e.title} tags={e.tags} />
|
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
|
||||||
{/each}
|
.map((s) => s.id_code)
|
||||||
|
.includes(t.id_code))) as e}
|
||||||
|
<Card bind:exo={e} />
|
||||||
|
{/each}
|
||||||
|
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@ -48,6 +111,7 @@
|
|||||||
grid-auto-flow: dense;
|
grid-auto-flow: dense;
|
||||||
grid-gap: 32px;
|
grid-gap: 32px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
68
frontend/src/components/exos/Head.svelte
Normal file
68
frontend/src/components/exos/Head.svelte
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TagSelector from '../forms/TagSelector.svelte';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import CreateCard from './CreateCard.svelte';
|
||||||
|
import { useQueryClient } from '@sveltestack/svelte-query';
|
||||||
|
import type { Page, Tag } from '../../types/exo.type';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { Store } from '../../types/api.type';
|
||||||
|
|
||||||
|
const { navigate } = getContext<{navigate: Function}>('navigation');
|
||||||
|
const { show, close } = getContext<{show: Function, close: Function}>('modal');
|
||||||
|
export let location = 'public';
|
||||||
|
export let search = '';
|
||||||
|
export let selected: Tag[] = [];
|
||||||
|
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||||
|
|
||||||
|
const tags: Writable<Store<Tag[]>> = getContext('tags')
|
||||||
|
const exerciceStore: Writable<Store<Page>> = getContext('exos');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="head">
|
||||||
|
<div class="new">
|
||||||
|
{#if !!isAuth}
|
||||||
|
<button
|
||||||
|
class="border-primary-btn"
|
||||||
|
on:click={() => {
|
||||||
|
show(CreateCard, {
|
||||||
|
cancel: () => {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
exos: exerciceStore
|
||||||
|
});
|
||||||
|
}}>Nouveau</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" placeholder="Rechercher" class="input" bind:value={search} />
|
||||||
|
{#if !!isAuth}
|
||||||
|
<TagSelector options={$tags.data} bind:selected />
|
||||||
|
<select
|
||||||
|
name="ee"
|
||||||
|
id="e"
|
||||||
|
class="input"
|
||||||
|
bind:value={location}
|
||||||
|
on:change={(e) => navigate(`/exercices/${e.currentTarget.value}`)}
|
||||||
|
>
|
||||||
|
<option value="user">Vos exos</option>
|
||||||
|
<option value="public">Tous les exos</option>
|
||||||
|
</select>{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
div {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,44 +1,77 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
export let exo;
|
import MdClose from 'svelte-icons/md/MdClose.svelte';
|
||||||
let examples = ['an example', 'an example', 'an example', 'an example', 'an example'];
|
import MdEdit from 'svelte-icons/md/MdEdit.svelte';
|
||||||
|
import MdDelete from 'svelte-icons/md/MdDelete.svelte';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
import DownloadForm from './DownloadForm.svelte';
|
||||||
|
import EditForm from './EditForm.svelte';
|
||||||
|
import type { Exercice, Page, Tag } from '../../types/exo.type';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { Store } from '../../types/api.type';
|
||||||
|
export let exo: Exercice;
|
||||||
|
export let exos: Writable<Store<Page>>;
|
||||||
|
export let tags: Writable<Store<Tag[]>>;
|
||||||
|
setContext("tags", tags)
|
||||||
|
let editing = false;
|
||||||
|
|
||||||
|
const updateExo = (e: Exercice) => {
|
||||||
|
exos.update((o) => {
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
data: {
|
||||||
|
...o.data,
|
||||||
|
items: [
|
||||||
|
...o.data.items.map((ex) => {
|
||||||
|
if (ex.id_code == exo.id_code) {
|
||||||
|
return e;
|
||||||
|
} return ex;
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h1>Titre</h1>
|
{#if editing === false}
|
||||||
<input type="text" class="input" />
|
<DownloadForm
|
||||||
<div class="examples">
|
{exo}
|
||||||
<h2>Exemples</h2>
|
delete_={() => {
|
||||||
{#each examples as e}
|
exos.update((o) => {
|
||||||
<p>{e}</p>
|
return {
|
||||||
{/each}
|
...o,
|
||||||
</div>
|
data: { ...o.data, items: [...o.data.items.filter((e) => e.id_code != exo.id_code)] }
|
||||||
<button>Télécharger</button>
|
};
|
||||||
<div class="tags" />
|
});
|
||||||
|
}}
|
||||||
|
edit={() => {
|
||||||
|
editing = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if editing === true}
|
||||||
|
<EditForm
|
||||||
|
bind:exo
|
||||||
|
cancel={() => {
|
||||||
|
editing = false;
|
||||||
|
}}
|
||||||
|
{updateExo}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
.modal {
|
.modal {
|
||||||
min-width: 820px;
|
min-width: 820px;
|
||||||
display: grid;
|
background: $background;
|
||||||
grid-template-columns: 1fr;
|
padding: 70px;
|
||||||
grid-template-rows: repeat(5, auto);
|
grid-gap: 10px;
|
||||||
background: blue;
|
position: relative;
|
||||||
padding:70px;
|
display: flex;
|
||||||
grid-gap:10px;
|
flex-direction: column;
|
||||||
}
|
align-items: flex-start;
|
||||||
h1 {
|
gap: 20px;
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.examples {
|
|
||||||
h2 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 5px 0;
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
35
frontend/src/components/exos/Pagination.svelte
Normal file
35
frontend/src/components/exos/Pagination.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {page as p} from '$app/stores'
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
export let page: number;
|
||||||
|
export let total: number;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
{#each Array(total) as _, i}
|
||||||
|
<p
|
||||||
|
class:active={page == i + 1}
|
||||||
|
on:click={() => {
|
||||||
|
page = i + 1;
|
||||||
|
$p.url.searchParams.set('page', String(i+1))
|
||||||
|
goto(`?${$p.url.searchParams.toString()}`);
|
||||||
|
}}
|
||||||
|
on:keydown = {()=>{}}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.active {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
margin: 30px;
|
||||||
|
p {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
45
frontend/src/components/exos/PrivacyIndicator.svelte
Normal file
45
frontend/src/components/exos/PrivacyIndicator.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let color: 'red' | "green" | 'blue'= "red";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class={color}><slot /></p>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
|
.red {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
.green{
|
||||||
|
color: $green;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
color: $contrast;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 0.8em;
|
||||||
|
height: 18px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.2;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
&:hover::after{
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
85
frontend/src/components/exos/Tag.svelte
Normal file
85
frontend/src/components/exos/Tag.svelte
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
export let label: string;
|
||||||
|
export let color: string;
|
||||||
|
export let remove: Function;
|
||||||
|
let removed = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:removed class="selected" style={`--item-color:${chroma(color).rgb().join(',')};`}>
|
||||||
|
<div class="label">{label}</div>
|
||||||
|
<div
|
||||||
|
class="unselect"
|
||||||
|
on:click={() => {
|
||||||
|
removed = true;
|
||||||
|
remove()
|
||||||
|
/* setTimeout(() => {
|
||||||
|
if(!remove()){
|
||||||
|
removed=false
|
||||||
|
}
|
||||||
|
}, 300); */
|
||||||
|
}}
|
||||||
|
on:keypress={() => {}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
height="14"
|
||||||
|
width="14"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
class="css-8mmkcg"
|
||||||
|
><path
|
||||||
|
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.unselect {
|
||||||
|
color: rgb(var(--item-color));
|
||||||
|
-moz-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: currentColor;
|
||||||
|
svg {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: rgba(var(--item-color), 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
margin: 2px;
|
||||||
|
min-width: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: 0.5s;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removed {
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: rgb(var(--item-color));
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 85%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 3px 3px 3px 6px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
165
frontend/src/components/exos/TagContainer.svelte
Normal file
165
frontend/src/components/exos/TagContainer.svelte
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
|
||||||
|
import { addTags, delTags } from '../../requests/exo.request';
|
||||||
|
import type { Exercice, Tag as TagType } from '../../types/exo.type';
|
||||||
|
import TagSelector from '../forms/TagSelector.svelte';
|
||||||
|
import Tag from './Tag.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import type { Store } from '../../types/api.type';
|
||||||
|
export let exo: Exercice;
|
||||||
|
let tg = false;
|
||||||
|
let tagMode = false;
|
||||||
|
let selected: { label: string; id_code: string; color: string, created?: boolean }[] = [];
|
||||||
|
export let tags: Writable<Store<TagType[]>> = getContext('tags');
|
||||||
|
console.log('TAGS +', tags, getContext('test'))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tags-container"
|
||||||
|
class:tg
|
||||||
|
class:tagMode
|
||||||
|
on:click|stopPropagation={() => {}}
|
||||||
|
on:keypress={() => {}}
|
||||||
|
>
|
||||||
|
{#if tg === false}
|
||||||
|
<div class="tags">
|
||||||
|
{#each exo.tags as t}
|
||||||
|
<Tag
|
||||||
|
color={t.color}
|
||||||
|
remove={() => {
|
||||||
|
delTags(exo.id_code, t.id_code).then((r) => {
|
||||||
|
exo.tags = r.tags;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
label={t.label}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="expand"
|
||||||
|
on:click={() => {
|
||||||
|
tg = true;
|
||||||
|
console.log('TAGGGG', $tags)
|
||||||
|
setTimeout(() => {
|
||||||
|
tagMode = true;
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
|
on:keydown={() => {}}
|
||||||
|
>
|
||||||
|
<p>+</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<TagSelector disabled={exo.tags} bind:selected options={$tags.data} creatable allowEditing />
|
||||||
|
<button
|
||||||
|
class="primary-btn"
|
||||||
|
on:click|preventDefault={() => {
|
||||||
|
addTags(
|
||||||
|
exo.id_code,
|
||||||
|
selected.map((t) => {
|
||||||
|
delete t.created;
|
||||||
|
return t;
|
||||||
|
})
|
||||||
|
).then((r) => {
|
||||||
|
exo.tags = r.exo.tags;
|
||||||
|
if (r.tags.length != 0) {
|
||||||
|
tags.update((tt) => {
|
||||||
|
return { ...tt, data: [...tt.data, ...r.tags] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tg = false;
|
||||||
|
tagMode = false;
|
||||||
|
});
|
||||||
|
}}>Valider !</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="danger-btn"
|
||||||
|
on:click={() => {
|
||||||
|
tagMode = false;
|
||||||
|
tg = false;
|
||||||
|
}}>Annuler</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
* {
|
||||||
|
transition: 0.45s;
|
||||||
|
}
|
||||||
|
.tags-container {
|
||||||
|
min-height: 30px;
|
||||||
|
position: absolute;
|
||||||
|
padding: 1% 2%;
|
||||||
|
padding-right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: darken($background, $amount: 7);
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
border: 1px solid darken($background, $amount: 7);
|
||||||
|
border-top: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 85% auto;
|
||||||
|
grid-gap: 8px;
|
||||||
|
}
|
||||||
|
.tg {
|
||||||
|
transition: 0.3s;
|
||||||
|
min-height: 100%;
|
||||||
|
:global(> *) {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
:global(> div) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.expand {
|
||||||
|
padding: 0 15px;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover p {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
position: relative;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background-color: black;
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tagMode {
|
||||||
|
transition: 0.5s;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
:global(> *) {
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
:global(> div),
|
||||||
|
:global(> button) {
|
||||||
|
flex-grow: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
20
frontend/src/components/forms/DropdownItem.svelte
Normal file
20
frontend/src/components/forms/DropdownItem.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
|
||||||
|
export let option: { label: string; color: string };
|
||||||
|
export let idx;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style={`--color: ${chroma(option.color).rgb().join(',')}`}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
color: rgb(var(--color));
|
||||||
|
padding: 10px;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(var(--color), 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
96
frontend/src/components/forms/FileInput.svelte
Normal file
96
frontend/src/components/forms/FileInput.svelte
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getExoSource } from '../../requests/exo.request';
|
||||||
|
import MdFileDownload from 'svelte-icons/md/MdFileDownload.svelte';
|
||||||
|
export let label = 'Choisir un fichier';
|
||||||
|
export let value: FileList;
|
||||||
|
export let id_code: string | null = null;
|
||||||
|
|
||||||
|
const id = String(Math.random());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="fileinput">
|
||||||
|
<input type="file" {id} {...$$restProps} bind:files={value} />
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
<div class="filename">
|
||||||
|
{#if value.length !== 0}
|
||||||
|
<p>{value[0].name}</p>
|
||||||
|
{#if id_code != null}
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:click={() => {
|
||||||
|
if (id_code == null) return;
|
||||||
|
getExoSource(id_code);
|
||||||
|
}}
|
||||||
|
on:keydown={() => {}}
|
||||||
|
>
|
||||||
|
<MdFileDownload />
|
||||||
|
</div>{/if}
|
||||||
|
{:else}
|
||||||
|
...
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fileinput {
|
||||||
|
display: flex;
|
||||||
|
border: yellow 1px solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
& + label {
|
||||||
|
font-size: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
color: black;
|
||||||
|
background-color: yellow;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background-color: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filename {
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 100px;
|
||||||
|
color: yellow;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
p {
|
||||||
|
max-width: 200px;
|
||||||
|
margin: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
138
frontend/src/components/forms/InputWithLabel.svelte
Normal file
138
frontend/src/components/forms/InputWithLabel.svelte
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import IoMdEye from 'svelte-icons/io/IoMdEye.svelte';
|
||||||
|
import IoMdEyeOff from 'svelte-icons/io/IoMdEyeOff.svelte';
|
||||||
|
export let type = 'text';
|
||||||
|
export let value = '';
|
||||||
|
export let label = '';
|
||||||
|
export let errors: string[] = [];
|
||||||
|
function typeAction(node: HTMLInputElement) {
|
||||||
|
node.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
let show = type != 'password';
|
||||||
|
const id = String(Math.random());
|
||||||
|
const toggle = () => {
|
||||||
|
const element = document.getElementById(id) as HTMLInputElement;
|
||||||
|
if (element === null) return;
|
||||||
|
element.type = show === true ? 'password' : 'text';
|
||||||
|
show = !show;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="inputLabel" class:error={errors.length !== 0}>
|
||||||
|
<input use:typeAction {id} bind:value {...$$restProps} placeholder="" />
|
||||||
|
<!-- placeholder = "" pour que le label se place bien avec :placeholder-shown -->
|
||||||
|
<label for={id}>{label}</label>
|
||||||
|
{#if type == 'password'}
|
||||||
|
<div class="toggle" on:click={toggle} on:keypress={() => {}}>
|
||||||
|
{#if show == false}
|
||||||
|
<IoMdEyeOff />
|
||||||
|
{:else if show == true}
|
||||||
|
<IoMdEye />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="bar" />
|
||||||
|
|
||||||
|
{#if errors.length !== 0}
|
||||||
|
<p class="error-msg">{errors[0]}</p>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../variables';
|
||||||
|
.error-msg {
|
||||||
|
color: $red;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
right: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.inputLabel {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 10px 10px 5px;
|
||||||
|
border-bottom: 1px solid $input-border;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #f8f8f8;
|
||||||
|
& ~ label {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: normal;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 5px;
|
||||||
|
top: 10px;
|
||||||
|
transition: 0.3s ease all;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #8e8e8e;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&:focus ~ label,
|
||||||
|
&:not(:placeholder-shown) ~ label {
|
||||||
|
top: -0.8em;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $contrast;
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
&:disabled ~ label {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
&:focus ~ .bar:before {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $red;
|
||||||
|
& input {
|
||||||
|
color: $red;
|
||||||
|
border-bottom: 1px solid $red;
|
||||||
|
&:focus ~ label,
|
||||||
|
&:not(:placeholder-shown):valid ~ label {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
&:focus ~ .bar::before {
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
height: 2px;
|
||||||
|
width: 0;
|
||||||
|
bottom: 0px;
|
||||||
|
position: absolute;
|
||||||
|
background: $contrast;
|
||||||
|
transition: 0.3s ease all;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
130
frontend/src/components/forms/Item.svelte
Normal file
130
frontend/src/components/forms/Item.svelte
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { itemActions, highlightSearch, CloseButton } from 'svelecte/item';
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
// these properties can be used
|
||||||
|
//export let inputValue;
|
||||||
|
export let index = -1;
|
||||||
|
export let item: { label: string; id_code: string; color: string };
|
||||||
|
export let isSelected = false;
|
||||||
|
export let isDisabled = false;
|
||||||
|
//export let isMultiple = false;
|
||||||
|
|
||||||
|
const color = chroma(item.color);
|
||||||
|
console.log(color.rgb(), color);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- you need to use itemActions and pass given events -->
|
||||||
|
<div
|
||||||
|
class="sv-item"
|
||||||
|
use:itemActions={{ item, index }}
|
||||||
|
class:is-selected={isSelected}
|
||||||
|
on:select
|
||||||
|
on:deselect
|
||||||
|
on:hover
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={`sv-item-content`}
|
||||||
|
style={`--item-color:${chroma(item.color).rgb().join(',')};`}
|
||||||
|
class:disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{#if !isSelected}
|
||||||
|
{item.label}
|
||||||
|
{:else}
|
||||||
|
<div class="selected">
|
||||||
|
<div class="label">{item.label}</div>
|
||||||
|
<div class="unselect" data-action="deselect">
|
||||||
|
<svg
|
||||||
|
height="14"
|
||||||
|
width="14"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
class="css-8mmkcg"
|
||||||
|
><path
|
||||||
|
d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.unselect {
|
||||||
|
color: rgb(var(--item-color));
|
||||||
|
-moz-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: currentColor;
|
||||||
|
svg {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: rgba(var(--item-color), 0.1);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
margin: 2px;
|
||||||
|
min-width: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: rgb(var(--item-color));
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 85%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 3px 3px 3px 6px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/** some style override */
|
||||||
|
.sv-item {
|
||||||
|
background-color: $background!important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.sv-item-content {
|
||||||
|
color: rgb(var(--item-color));
|
||||||
|
}
|
||||||
|
:global(.sv-dropdown-content) .sv-item-content {
|
||||||
|
padding: 13px;
|
||||||
|
}
|
||||||
|
:global(.sv-dropdown-content) .sv-item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
:global(.sv-dd-item-active) .sv-item-content:not(.disabled) {
|
||||||
|
background-color: rgba(var(--item-color), 0.1);
|
||||||
|
color: rgb(var(--item-color)) !important;
|
||||||
|
}
|
||||||
|
:global(.sv-dd-item-active) .sv-dd-item{
|
||||||
|
background-color: var(--sv-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sv-content) .sv-item-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
:global(.sv-content) .sv-item {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.disabled,
|
||||||
|
:global(.sv-dd-item-active) .sv-item-content.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: grey;
|
||||||
|
background-color: var(--sv-bg);
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
15
frontend/src/components/forms/SelectedItem.svelte
Normal file
15
frontend/src/components/forms/SelectedItem.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<script lang='ts'>
|
||||||
|
import chroma from 'chroma-js'
|
||||||
|
|
||||||
|
export let option: {label: string, color: string};
|
||||||
|
export let idx;
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{option.label} {option.color}
|
||||||
|
<button>test</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
58
frontend/src/components/forms/TagSelector.svelte
Normal file
58
frontend/src/components/forms/TagSelector.svelte
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Svelecte, { addFormatter } from 'svelecte';
|
||||||
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
import Item from './Item.svelte';
|
||||||
|
export let options: { label: string; id_code: string; color: string }[];
|
||||||
|
export let disabled: { label: string; id_code: string; color: string }[] = [];
|
||||||
|
export let selected: { label: string; id_code: string; color: string }[] =[];
|
||||||
|
|
||||||
|
//$: options = options.filter(o=>!selected.map(s=>s.id_code).includes(o.id_code))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Svelecte
|
||||||
|
options={options.map((o) => {
|
||||||
|
if (disabled.map((s) => s.id_code).includes(o.id_code)) {
|
||||||
|
return { ...o, $disabled: true };
|
||||||
|
}
|
||||||
|
return {...o};
|
||||||
|
}).sort(o=> o.$disabled == true ? 1:0)}
|
||||||
|
controlItem={Item}
|
||||||
|
dropdownItem={Item}
|
||||||
|
valueField={'id_code'}
|
||||||
|
createTransform={(inputValue, creatablePrefix, valueField, labelField) => {
|
||||||
|
return {
|
||||||
|
label: inputValue,
|
||||||
|
id_code: Math.random().toString(36).substring(2, 9),
|
||||||
|
color: '#00ff00',
|
||||||
|
created: true
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
on:change={
|
||||||
|
(s)=>{
|
||||||
|
console.log('CHANGES', s)
|
||||||
|
selected = s.detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on:createoption={(opt) => {
|
||||||
|
console.log('NEW OPT', opt);
|
||||||
|
|
||||||
|
selected.push({ ...opt.detail });
|
||||||
|
}}
|
||||||
|
|
||||||
|
multiple
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "../../variables";
|
||||||
|
:global(.sv-dropdown-scroll) {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
:global(.svelecte-control) {
|
||||||
|
--sv-bg: $background!important;
|
||||||
|
--sv-border-color: green !important;
|
||||||
|
--sv-item-active-bg: $background!important;
|
||||||
|
--sv-border: none!important;
|
||||||
|
--sv-active-border:(1px solid $contrast)!important;
|
||||||
|
}
|
||||||
|
</style>
|
150
frontend/src/context/Alert.svelte
Normal file
150
frontend/src/context/Alert.svelte
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
let visible: boolean = false;
|
||||||
|
let title: string = '';
|
||||||
|
let description: string = '';
|
||||||
|
let validateButton: string = '';
|
||||||
|
let validate: Function = () => {};
|
||||||
|
|
||||||
|
const alert = (newAlert: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
validate: Function;
|
||||||
|
validateButton?: string;
|
||||||
|
}) => {
|
||||||
|
title = newAlert.title;
|
||||||
|
description = newAlert.description;
|
||||||
|
validateButton = newAlert.validateButton || 'Oui';
|
||||||
|
validate = newAlert.validate;
|
||||||
|
visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
setContext('alert', { alert });
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPress = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div class="overlay" on:click={() => close()} class:visible on:keypress={keyPress} />
|
||||||
|
|
||||||
|
<div id="modal" class:visible on:keypress={keyPress}>
|
||||||
|
<div class="head">
|
||||||
|
<div>
|
||||||
|
<p>{title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="desc">
|
||||||
|
<p>{description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
validate();
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
class="valid">Oui</button
|
||||||
|
>
|
||||||
|
<button on:click={close} class="cancel">Non</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.valid {
|
||||||
|
background-color: rgba(255, 79, 100, 0.2);
|
||||||
|
color: rgb(255, 79, 100);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(199, 38, 57, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cancel {
|
||||||
|
background-color: rgba(13, 2, 33, 0.3);
|
||||||
|
color: white;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(32, 5, 82, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
width: 100%;
|
||||||
|
padding: 26px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
background-color: #ff4f64;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #1d1a5a;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
background-color: #1d1a5a;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 24px 30px 24px;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
button {
|
||||||
|
min-height: 36px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
margin-left: 2%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: 0.3s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
//width: 50%;
|
||||||
|
max-width: 600px;
|
||||||
|
min-width: 500px;
|
||||||
|
transform: translateX(-50%) translateY(-50%) scale(0.7);
|
||||||
|
visibility: hidden;
|
||||||
|
transition: 0.4s;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
&.visible {
|
||||||
|
visibility: visible !important;
|
||||||
|
transform: translateX(-50%) translateY(-50%) scale(1) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1999;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
&.visible {
|
||||||
|
opacity: 0.7;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
40
frontend/src/context/Auth.svelte
Normal file
40
frontend/src/context/Auth.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, setContext } from 'svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { loginRequest, refreshRequest, registerRequest } from '../requests/auth.request';
|
||||||
|
import jwt_decode from 'jwt-decode';
|
||||||
|
const user = writable(null);
|
||||||
|
const isAuth = writable(false);
|
||||||
|
const login = (login: string, password: string) => {
|
||||||
|
loginRequest({ login, password }).then((r) => {
|
||||||
|
localStorage.setItem('token', `${r.access_token}`);
|
||||||
|
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||||
|
isAuth.set(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const register = (username: string, password: string, confirm: string) => {
|
||||||
|
registerRequest({ username, password, password_confirm: confirm }).then((r) => {
|
||||||
|
localStorage.setItem('token', `${r.access_token}`);
|
||||||
|
localStorage.setItem('refresh', `${r.refresh_token}`);
|
||||||
|
isAuth.set(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const logout = () => {};
|
||||||
|
setContext('auth', { user, isAuth, login, register, logout });
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (localStorage.getItem('token') != null) {
|
||||||
|
const { exp } = jwt_decode(localStorage.getItem('token')!);
|
||||||
|
console.log(Date.now(), exp, Date.now() >= exp * 1000)
|
||||||
|
if (Date.now() >= exp * 1000) {
|
||||||
|
refreshRequest(localStorage.getItem('refresh')!).then(r=>{localStorage.setItem('token', r.access_token)})
|
||||||
|
isAuth.set(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isAuth.set(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>{$isAuth ? 'Connecté' : 'Non connecté'}</p>
|
||||||
|
<slot />
|
@ -1,69 +1,105 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, setContext, SvelteComponent } from 'svelte';
|
import { onDestroy, setContext, SvelteComponent } from 'svelte';
|
||||||
|
|
||||||
let visible = false;
|
let visible = false;
|
||||||
let onClose: Function;
|
let props = {};
|
||||||
let props = {};
|
let component: ConstructorOfATypedSvelteComponent | undefined;
|
||||||
let component: ConstructorOfATypedSvelteComponent | undefined;
|
let closed = true;
|
||||||
function show(c : ConstructorOfATypedSvelteComponent, p: Object) {
|
let onClose: Function = () => {};
|
||||||
visible = true
|
function show(
|
||||||
component = c
|
c: ConstructorOfATypedSvelteComponent,
|
||||||
props = p
|
p: Object,
|
||||||
}
|
newOnClose: Function = () => {},
|
||||||
function close(){
|
editing: boolean = false
|
||||||
visible=false
|
) {
|
||||||
component = undefined;
|
if (editing == false && closed === false) return;
|
||||||
props = {}
|
visible = true;
|
||||||
onClose()
|
closed = false;
|
||||||
}
|
component = c;
|
||||||
setContext('modal', {show, close})
|
console.log('edi', editing, c, p)
|
||||||
|
if (editing) {
|
||||||
|
console.log('EDITINGF', props)
|
||||||
|
props = { ...props, ...p };
|
||||||
|
console.log(props)
|
||||||
|
} else {
|
||||||
|
props = p;
|
||||||
|
}
|
||||||
|
onClose = newOnClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addContext (key: string, value: any) {
|
||||||
|
setContext(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false;
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
component = undefined;
|
||||||
|
props = {};
|
||||||
|
closed = true;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
setContext('modal', { show, close, addContext });
|
||||||
function keyPress(e: KeyboardEvent) {
|
function keyPress(e: KeyboardEvent) {
|
||||||
|
console.log('HOP');
|
||||||
if (e.key == 'Escape' && visible == true) {
|
if (e.key == 'Escape' && visible == true) {
|
||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
<div id="topModal" class:visible on:click={() => close()} on:keypress={keyPress}>
|
|
||||||
<div id="modal" on:click|stopPropagation={()=>{}} on:keypress={()=>{}}>
|
<div
|
||||||
<svelte:component this={component} {...props}/>
|
class="overlay"
|
||||||
</div>
|
on:click={() => close()}
|
||||||
|
class:visible={!closed}
|
||||||
|
class:closing={!visible && !closed}
|
||||||
|
on:keypress={keyPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div id="modal" class:visible on:keypress={keyPress}>
|
||||||
|
<svelte:component this={component} {...props} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
<style>
|
#modal {
|
||||||
#topModal {
|
|
||||||
visibility: hidden;
|
|
||||||
z-index: 9999;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%) scale(0.7);
|
||||||
|
visibility: hidden;
|
||||||
|
transition: 0.4s;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
&.visible {
|
||||||
|
visibility: visible !important;
|
||||||
|
transform: translateX(-50%) translateY(-50%) scale(1) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
background-color: black;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 999;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: #4448;
|
visibility: hidden;
|
||||||
display: flex;
|
transition: 0.3s;
|
||||||
align-items: center;
|
&.visible {
|
||||||
justify-content: center;
|
opacity: 0.7;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#modal {
|
.closing {
|
||||||
position: relative;
|
opacity: 0 !important;
|
||||||
border-radius: 6px;
|
|
||||||
background: white;
|
|
||||||
border: 2px solid #000;
|
|
||||||
filter: drop-shadow(5px 5px 5px #555);
|
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* #modal-content {
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
max-height: calc(100vh - 20px);
|
|
||||||
overflow: auto;
|
|
||||||
} */
|
|
||||||
</style>
|
</style>
|
||||||
|
42
frontend/src/context/Navigation.svelte
Normal file
42
frontend/src/context/Navigation.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { goto, afterNavigate } from '$app/navigation';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
|
let previous: string | null = base;
|
||||||
|
let first = true;
|
||||||
|
const navigate = (
|
||||||
|
url: string | number,
|
||||||
|
params: object | undefined,
|
||||||
|
options:
|
||||||
|
| {
|
||||||
|
replaceState?: boolean | undefined;
|
||||||
|
noScroll?: boolean | undefined;
|
||||||
|
keepFocus?: boolean | undefined;
|
||||||
|
state?: any;
|
||||||
|
invalidateAll?: boolean | undefined;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
) => {
|
||||||
|
if (browser) {
|
||||||
|
console.log('PREVIOUS', previous, typeof url == 'number', previous);
|
||||||
|
if (typeof url == 'number' && previous != null) {
|
||||||
|
goto(previous);
|
||||||
|
} else {
|
||||||
|
const parsedParams = new URLSearchParams(params as Record<string, string>);
|
||||||
|
goto(`${url}?${parsedParams.toString()}`, { ...options });
|
||||||
|
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
afterNavigate(({ from }) => {
|
||||||
|
previous = from?.url.toString() || previous;
|
||||||
|
});
|
||||||
|
|
||||||
|
setContext('navigation', { navigate });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
42
frontend/src/requests/auth.request.ts
Normal file
42
frontend/src/requests/auth.request.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const loginRequest = (data: { login: string; password: string }) => {
|
||||||
|
return axios({
|
||||||
|
url: 'http://localhost:8002/login',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
.then((r) => r.data as {access_token: string, refresh_token: string, token_type: string })
|
||||||
|
.catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const registerRequest = (data: { username: string; password: string, password_confirm: string }) => {
|
||||||
|
return axios({
|
||||||
|
url: 'http://localhost:8002/register',
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((r) => r.data as { access_token: string; refresh_token: string; token_type: string })
|
||||||
|
.catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshRequest = (token: string) => {
|
||||||
|
return axios({
|
||||||
|
url: 'http://localhost:8002/refresh',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((r) => r.data as {access_token:string})
|
||||||
|
.catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
134
frontend/src/requests/exo.request.ts
Normal file
134
frontend/src/requests/exo.request.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import type { Tag } from '..//types/exo.type';
|
||||||
|
import { exoInstance } from '../apis/exo.api';
|
||||||
|
import { stringify } from 'qs';
|
||||||
|
const formDataFromObj = (obj: object) => {
|
||||||
|
const form_data = new FormData();
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
form_data.append(key, obj[key as keyof typeof obj]);
|
||||||
|
}
|
||||||
|
return form_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createExo = (data: {
|
||||||
|
name: string;
|
||||||
|
consigne: string;
|
||||||
|
private: boolean;
|
||||||
|
file: File;
|
||||||
|
}) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: '/exercices',
|
||||||
|
method: 'POST',
|
||||||
|
data: formDataFromObj(data),
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const delExo = (id_code: string) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: '/exercice/' + id_code,
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editExo = (
|
||||||
|
id_code: string,
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
consigne: string;
|
||||||
|
private: boolean;
|
||||||
|
file?: File;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: '/exercice/' + id_code,
|
||||||
|
method: 'PUT',
|
||||||
|
data: formDataFromObj(data),
|
||||||
|
headers: {
|
||||||
|
'Content-type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExos = (
|
||||||
|
category: 'public' | 'user',
|
||||||
|
data: {
|
||||||
|
search?: string;
|
||||||
|
type?: 'csv' | 'pdf' | 'web';
|
||||||
|
tags?: string[];
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
console.log('SENDINF', data, stringify(data, { arrayFormat: 'brackets' }));
|
||||||
|
return exoInstance({
|
||||||
|
url: '/exercices/' + category,
|
||||||
|
method: 'GET',
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
.then((r) => r.data)
|
||||||
|
.catch(console.log);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExo = (id_code: string) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: '/exercice/' + id_code,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
.then((r) => r.data)
|
||||||
|
.catch(console.log);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addTags = (id_code: string, data: Tag[]) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: `/exercice/${id_code}/tags`,
|
||||||
|
data,
|
||||||
|
method: 'POST'
|
||||||
|
}).then((r) => r.data);
|
||||||
|
};
|
||||||
|
export const delTags = (id_code: string, tag_id: string) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: `/exercice/${id_code}/tags/${tag_id}`,
|
||||||
|
method: 'DELETE'
|
||||||
|
}).then((r) => r.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTags = () => {
|
||||||
|
return exoInstance({
|
||||||
|
url: `/tags`,
|
||||||
|
method: 'Get'
|
||||||
|
}).then((r) => r.data).catch(console.log);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloneExo = (id_code: string) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: `/clone/${id_code}`,
|
||||||
|
method: 'POST'
|
||||||
|
}).then((r) => r.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExoSource = (id_code: string,) => {
|
||||||
|
return exoInstance({
|
||||||
|
url: `/exercice/${id_code}/exo_source`,
|
||||||
|
method: 'Get'
|
||||||
|
}).then((r) => {
|
||||||
|
const contentDisposition = r.headers['content-disposition'] || "filename=untitled.py";
|
||||||
|
const splitted = contentDisposition.split('filename=')
|
||||||
|
let filename = "untitled.py"
|
||||||
|
if(splitted.length >= 1) {
|
||||||
|
filename = splitted[1]
|
||||||
|
}
|
||||||
|
const blob = new Blob([r.data], {
|
||||||
|
type: 'text/x-python'
|
||||||
|
});
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
});
|
||||||
|
};
|
@ -2,20 +2,35 @@
|
|||||||
import Modal from '../context/Modal.svelte';
|
import Modal from '../context/Modal.svelte';
|
||||||
import NavLink from '../components/NavLink.svelte';
|
import NavLink from '../components/NavLink.svelte';
|
||||||
import '../app.scss';
|
import '../app.scss';
|
||||||
|
import Alert from '../context/Alert.svelte';
|
||||||
|
import Auth from '../context/Auth.svelte';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query';
|
||||||
|
import { Router } from 'svelte-navigator';
|
||||||
|
import Navigation from '../context/Navigation.svelte';
|
||||||
|
const queryClient = new QueryClient();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal>
|
<Navigation>
|
||||||
<main>
|
<QueryClientProvider client={queryClient}>
|
||||||
<nav data-sveltekit-preload-data="hover">
|
<Auth>
|
||||||
<NavLink href="/" exact>Home</NavLink>
|
<Alert>
|
||||||
<NavLink href="/exercices" exact>Exercices</NavLink>
|
<Modal>
|
||||||
<NavLink href="/settings" exact>Settings</NavLink>
|
<main>
|
||||||
</nav>
|
<nav data-sveltekit-preload-data="hover">
|
||||||
<slot />
|
<NavLink href="/" exact>Home</NavLink>
|
||||||
</main>
|
<NavLink href="/exercices" exact>Exercices</NavLink>
|
||||||
</Modal>
|
<NavLink href="/settings" exact>Settings</NavLink>
|
||||||
|
</nav>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Modal>
|
||||||
|
</Alert>
|
||||||
|
</Auth>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</Navigation>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../variables';
|
||||||
.links {
|
.links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -41,9 +56,9 @@
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #1d1a5a;
|
background-color: $background;
|
||||||
color: #d9d9d9;
|
color: #d9d9d9;
|
||||||
background: linear-gradient(to bottom left, #0d0221 30%, #1a0f7a);
|
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
@ -54,6 +69,7 @@
|
|||||||
padding-right: calc(50% - var(--container-width) / 2);
|
padding-right: calc(50% - var(--container-width) / 2);
|
||||||
height: calc(100vh - var(--navbar-height) - 10px);
|
height: calc(100vh - var(--navbar-height) - 10px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
a {
|
a {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
@ -65,7 +81,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding: 30px 0;
|
padding: 30px 0;
|
||||||
border-bottom: 1px solid #181553;
|
border-bottom: 1px solid $border;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import Card from '../components/exos/Card.svelte';
|
import { getExos } from '../requests/exo.request';
|
||||||
import { getContext } from 'svelte';
|
import { useQuery } from '@sveltestack/svelte-query';
|
||||||
let count = 1;
|
import { browser } from '$app/environment'
|
||||||
const { show } = getContext('modal');
|
const t = useQuery('tst',async ()=>{
|
||||||
|
return getExos('public' , {})
|
||||||
const add = () => {
|
});
|
||||||
show(Card, { title: 'test', examples: ['test'], tags: [], click: () => {} });
|
$: console.log("DATA", $t.data)
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Welcome to SvelteKit</h1>
|
<button
|
||||||
<div>
|
on:click={() => {
|
||||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
console.log(getExos('public', {}));
|
||||||
<button on:click={add}>test {count}</button>
|
}}>test</button
|
||||||
</div>
|
>
|
||||||
|
20
frontend/src/routes/signup/+page.svelte
Normal file
20
frontend/src/routes/signup/+page.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang='ts'>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
let u = '';
|
||||||
|
let p = '';
|
||||||
|
let p2 = "";
|
||||||
|
const {register} = getContext('auth')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="text" bind:value={u}>
|
||||||
|
<input type="text" bind:value={p}>
|
||||||
|
<input type="text" bind:value={p2}>
|
||||||
|
<button on:click={()=>{
|
||||||
|
register(u, p, p2)
|
||||||
|
}}>
|
||||||
|
register
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style lang='scss'>
|
||||||
|
|
||||||
|
</style>
|
7
frontend/src/types/api.type.ts
Normal file
7
frontend/src/types/api.type.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type Store<T> = {
|
||||||
|
isLoading: boolean,
|
||||||
|
isFetching: boolean,
|
||||||
|
isSuccess: boolean,
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
31
frontend/src/types/exo.type.ts
Normal file
31
frontend/src/types/exo.type.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export type Tag = {
|
||||||
|
label: string,
|
||||||
|
color: string,
|
||||||
|
id_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Exercice = {
|
||||||
|
csv: boolean,
|
||||||
|
pdf: boolean,
|
||||||
|
web: boolean,
|
||||||
|
name: string,
|
||||||
|
consigne: string,
|
||||||
|
private: boolean,
|
||||||
|
id_code: string,
|
||||||
|
exo_source: string,
|
||||||
|
author: { username: string },
|
||||||
|
original: {name: string, id_code: string, author: string} | null
|
||||||
|
tags: Tag[],
|
||||||
|
examples: { type: string, data: { calcul: string, correction: string }[] },
|
||||||
|
is_author: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Page ={
|
||||||
|
items: Exercice[],
|
||||||
|
total: number,
|
||||||
|
page: number,
|
||||||
|
size: number,
|
||||||
|
totalPage: number,
|
||||||
|
hasMore: number
|
||||||
|
}
|
31
frontend/src/utils/forms.ts
Normal file
31
frontend/src/utils/forms.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const errorMsg = (
|
||||||
|
form: {
|
||||||
|
dirty: boolean;
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
hasError: (s: string) => boolean;
|
||||||
|
},
|
||||||
|
name: string
|
||||||
|
): string[] => {
|
||||||
|
return [
|
||||||
|
form.hasError(`${name}.required`) && 'Champ requis',
|
||||||
|
form.hasError(`${name}.min`) && 'Trop court',
|
||||||
|
form.hasError(`${name}.max`) && 'Trop long',
|
||||||
|
form.hasError(`${name}.url`) && 'Pas bonne url',
|
||||||
|
form.hasError(`${name}.between`) && 'Pas valide',
|
||||||
|
form.hasError(`${name}.matchField`) && 'Ca matche pas',
|
||||||
|
form.hasError(`${name}.not`) && 'Valeur impossible',
|
||||||
|
].filter((r) => typeof r === 'string') as string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkFile = () => {
|
||||||
|
return async (value: Array<File>) => {
|
||||||
|
console.log('VALIDATION', value)
|
||||||
|
if (value.length == 0) {
|
||||||
|
return { valid:false, name: 'required' };
|
||||||
|
}
|
||||||
|
const name = value[0].name.split('.');
|
||||||
|
const ext = name[name.length - 1];
|
||||||
|
return { valid: value[0].type == 'text/x-python' && ext == 'py', name: 'extension' };
|
||||||
|
};
|
||||||
|
};
|
16
frontend/src/utils/utils.ts
Normal file
16
frontend/src/utils/utils.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export const compareObject = (obj1: object, obj2: object) => {
|
||||||
|
const keys = Object.keys(obj1);
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
const v1 = obj1[key as keyof typeof obj1];
|
||||||
|
const v2 = obj2[key as keyof typeof obj2];
|
||||||
|
console.log(obj1, obj2, v1, v2, key)
|
||||||
|
if (v1 != undefined && v2 != undefined) {
|
||||||
|
if (v1 != v2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
@ -1 +1,23 @@
|
|||||||
/* Variables and mixins declared here will be available in all other SCSS files */
|
/* Variables and mixins declared here will be available in all other SCSS files */
|
||||||
|
$primary: #FCBF49;
|
||||||
|
$primary-dark: #f08336;
|
||||||
|
$on-primary: #080808;
|
||||||
|
$secondary: #DF2935;
|
||||||
|
$secondary-dark: #c40e21;
|
||||||
|
$on-secondary: #080808;
|
||||||
|
$contrast: #5396e7;
|
||||||
|
$input-border: #64619f;
|
||||||
|
$border: #181553;
|
||||||
|
|
||||||
|
$background: #1d1a5a;
|
||||||
|
$background-light: #1a0f7a;
|
||||||
|
$background-dark: #0D0221;
|
||||||
|
$red: rgb(255, 79, 100);
|
||||||
|
$green: #41cf7c;
|
||||||
|
$rouge: #a6333f;
|
||||||
|
$vert: #41cf7c;
|
||||||
|
$bleu: #045aff;
|
||||||
|
$blanc: white;
|
||||||
|
$orange: orange;
|
||||||
|
$marron: brown;
|
||||||
|
$dark-green: #00712c;
|
@ -2,15 +2,17 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||||||
|
|
||||||
/** @type {import('vite').UserConfig} */
|
/** @type {import('vite').UserConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
additionalData: "@use \"src/variables.scss\" as *;"
|
additionalData: '@use "src/variables.scss" as *;'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
optimizeDeps: { exclude: ['svelte-navigator'] }
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
transition: transform .3s;
|
transition: transform .3s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
.input-container {
|
.input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
Loading…
Reference in New Issue
Block a user