before style
This commit is contained in:
parent
ccd047dbb0
commit
43031d22fb
@ -2,7 +2,7 @@ from fastapi import Query, HTTPException, status
|
||||
import os
|
||||
import shutil
|
||||
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 database.auth.models import User
|
||||
from database.db import get_session
|
||||
@ -62,22 +62,23 @@ def clone_exo_db(exercice: Exercice, user: User, db: Session):
|
||||
db.refresh(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)
|
||||
|
||||
for key, value in exo_data.items():
|
||||
setattr(old_exo, key, value)
|
||||
|
||||
old_exo.csv = supports['csv']
|
||||
old_exo.pdf = supports['pdf']
|
||||
old_exo.web = supports['web']
|
||||
if supports is not None:
|
||||
old_exo.csv = supports['csv']
|
||||
old_exo.pdf = supports['pdf']
|
||||
old_exo.web = supports['web']
|
||||
|
||||
example = {
|
||||
"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
|
||||
}
|
||||
old_exo.examples = example
|
||||
example = {
|
||||
"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
|
||||
}
|
||||
old_exo.examples = example
|
||||
|
||||
if 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)
|
||||
return old_exo
|
||||
|
||||
|
||||
def delete_exo_db(exo: Exercice, db: Session):
|
||||
db.delete(exo)
|
||||
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.id_code == tag.id_code, Tag.label == tag.label))).first()
|
||||
if tag_db is not None:
|
||||
return tag_db
|
||||
return tag_db, False
|
||||
id_code = generate_unique_code(Tag, db)
|
||||
tag_db = Tag(**{**tag.dict(exclude_unset=True),
|
||||
'id_code': id_code, 'author_id': user.id})
|
||||
db.add(tag_db)
|
||||
db.commit()
|
||||
db.refresh(tag_db)
|
||||
return tag_db
|
||||
return tag_db, True
|
||||
|
||||
|
||||
def add_tags_db(exo: Exercice, tags: List[TagCreate], user: User, db: Session):
|
||||
new = []
|
||||
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)
|
||||
db.add(exo)
|
||||
db.commit()
|
||||
db.refresh(exo)
|
||||
return exo
|
||||
return exo, new
|
||||
|
||||
|
||||
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)):
|
||||
if tags is None:
|
||||
return None
|
||||
validated_tags = []
|
||||
for t in tags:
|
||||
tag = db.exec(select(Tag.id).where(Tag.id_code == t)).first()
|
||||
if tag is not None:
|
||||
validated_tags.append(tag)
|
||||
return validated_tags
|
||||
return []
|
||||
validated_tags = db.exec(
|
||||
select(Tag.id_code, Tag.id).where(col(Tag.id_code).in_(tags))).all()
|
||||
|
||||
return [t.id for t in validated_tags]
|
||||
|
||||
|
||||
#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):
|
||||
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,
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
class ExerciceOrigin(SQLModel):
|
||||
#id: int
|
||||
id_code: str
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
|
||||
@ -138,6 +133,13 @@ class Author(SQLModel):
|
||||
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 value
|
||||
|
||||
class ExerciceRead(ExerciceBase):
|
||||
class ExerciceRead(ExerciceBase, Supports):
|
||||
id_code: str
|
||||
id: int
|
||||
author_id:int
|
||||
exo_source: str
|
||||
author: User
|
||||
original: Optional[Exercice]
|
||||
tags: List[Tag]
|
||||
examples: Example
|
||||
|
||||
class ExerciceReadFull(ExerciceReadBase):
|
||||
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",
|
||||
"http://localhost",
|
||||
"http://localhost:8080",
|
||||
"http://localhost:5173"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
@ -45,6 +46,7 @@ app.add_middleware(
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=['*']
|
||||
)
|
||||
|
||||
admin = Admin(app, engine)
|
||||
|
@ -1,3 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
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 fastapi.responses import FileResponse
|
||||
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
|
||||
|
||||
router = APIRouter(tags=['exercices'])
|
||||
|
||||
class ExoType(str, Enum):
|
||||
@ -27,7 +30,7 @@ def filter_exo_by_tags(exos: List[tuple[Exercice, str]], tags: List[Tag]):
|
||||
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
|
||||
|
||||
|
||||
@ -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])
|
||||
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
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = select(Exercice, func.group_concat(
|
||||
ExercicesTagLink.tag_id))
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
|
||||
statement = select(Exercice)
|
||||
statement = statement.where(Exercice.author_id == user.id)
|
||||
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:
|
||||
statement = statement.where(Exercice.web == True)
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = statement.join(ExercicesTagLink).where(
|
||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
||||
|
||||
#exercices = db.exec(statement).all()
|
||||
for t in tags:
|
||||
sub = select(ExercicesTagLink).where(ExercicesTagLink.exercice_id==Exercice.id).where(
|
||||
ExercicesTagLink.tag_id == t).exists()
|
||||
statement = statement.where(sub)
|
||||
page = p(db, statement)
|
||||
exercices = page.items
|
||||
if tags is not None and len(tags) != 0:
|
||||
exercices = filter_exo_by_tags(exercices, tags)
|
||||
page.items = [
|
||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||
|
||||
return page
|
||||
|
||||
|
||||
@ -92,12 +86,7 @@ def get_public_exercices(user: User | None = Depends(get_current_user_optional),
|
||||
search, tags, type = queryFilters
|
||||
|
||||
if user is not None:
|
||||
if tags is not None and len(tags) != 0:
|
||||
statement = select(Exercice, func.group_concat(
|
||||
ExercicesTagLink.tag_id))
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
|
||||
statement = select(Exercice)
|
||||
statement = statement.where(Exercice.author_id != user.id)
|
||||
statement = statement.where(Exercice.private == False)
|
||||
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)
|
||||
if type == ExoType.web:
|
||||
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:
|
||||
statement = statement.join(ExercicesTagLink).where(
|
||||
col(ExercicesTagLink.tag_id).in_(tags)).group_by(ExercicesTagLink.exercice_id)
|
||||
|
||||
exercices = db.exec(statement).all()
|
||||
|
||||
if tags is not None and len(tags) != 0:
|
||||
exercices = filter_exo_by_tags(exercices, tags)
|
||||
|
||||
exercices = [
|
||||
page = p(db, statement)
|
||||
print('¨PAGE', page)
|
||||
exercices = page.items
|
||||
page.items = [
|
||||
serialize_exo(exo=e, user_id=user.id, db=db) for e in exercices]
|
||||
return exercices
|
||||
return page
|
||||
|
||||
else:
|
||||
statement = select(Exercice)
|
||||
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)
|
||||
if type == ExoType.web:
|
||||
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)
|
||||
@ -153,7 +146,7 @@ def update_exo(file: UploadFile = Depends(validate_file_optionnal), exo: Exercic
|
||||
if file:
|
||||
file_obj = file["file"].file._file
|
||||
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)
|
||||
|
||||
|
||||
@ -169,13 +162,19 @@ def delete_exercice(exercice: Exercice | bool | None = Depends(check_exercice_au
|
||||
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)):
|
||||
if exo is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail='Exercice introuvable')
|
||||
exo_obj = add_tags_db(exo, tags, user, db)
|
||||
return serialize_exo(exo=exo_obj, user_id=user.id, db=db)
|
||||
exo_obj, new = add_tags_db(exo, tags, user, 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'])
|
||||
|
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):
|
||||
|
||||
if user == None:
|
||||
token = test_register(client, username="lilian")['access']
|
||||
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}
|
||||
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):
|
||||
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']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv6= test_create(client, private=True, user={'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"})
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv2 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv3 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv4 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv5 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv6 = test_create(client, private=True, user={
|
||||
'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',
|
||||
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()['size'] == 10
|
||||
assert r.json()["items"] == [prv11, prv12]
|
||||
|
||||
|
||||
|
||||
def test_get_users_exos_page_up(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['access']
|
||||
token2 = test_register(client, username="lilian2")['access']
|
||||
|
||||
prv = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv2 = test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv3= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv4= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv5= test_create(client, private=True, user={'token': token1, 'username': "lilian"})
|
||||
prv6= test_create(client, private=True, user={'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"})
|
||||
|
||||
prv = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv2 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv3 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv4 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv5 = test_create(client, private=True, user={
|
||||
'token': token1, 'username': "lilian"})
|
||||
prv6 = test_create(client, private=True, user={
|
||||
'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',
|
||||
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"] == []
|
||||
|
||||
|
||||
|
||||
def test_get_user_with_search(client: TestClient):
|
||||
token1 = test_register(client, username="lilian")['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')
|
||||
print(r.json())
|
||||
assert r.json()['items'] == [{**public1, 'is_author': False},
|
||||
{**public2, 'is_author': False}]
|
||||
{**public2, 'is_author': False}]
|
||||
|
||||
|
||||
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"})
|
||||
|
||||
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):
|
||||
@ -630,7 +665,7 @@ def test_get_csv(client: TestClient):
|
||||
|
||||
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):
|
||||
@ -644,7 +679,7 @@ def test_get_pdf(client: TestClient):
|
||||
|
||||
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):
|
||||
@ -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})
|
||||
|
||||
r = client.get('/exercices/public', params={"type": "web"})
|
||||
|
||||
|
||||
assert r.json() == [{**exoWeb.json(), 'is_author': False}]
|
||||
|
||||
|
||||
def test_get_invalid_type(client: TestClient):
|
||||
|
||||
|
||||
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": {
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"@types/chroma-js": "^2.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"eslint": "^8.28.0",
|
||||
@ -21,13 +22,27 @@
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"sass": "^1.53.0",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.0.0",
|
||||
"sass": "^1.53.0",
|
||||
"svelte-preprocess": "^4.10.7"
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"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:
|
||||
'@sveltejs/adapter-auto': ^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/parser': ^5.45.0
|
||||
axios: ^1.2.2
|
||||
chroma-js: ^2.4.2
|
||||
eslint: ^8.28.0
|
||||
eslint-config-prettier: ^8.5.0
|
||||
eslint-plugin-svelte3: ^4.0.0
|
||||
jwt-decode: ^3.1.2
|
||||
prettier: ^2.8.0
|
||||
prettier-plugin-svelte: ^2.8.1
|
||||
qs: ^6.11.0
|
||||
sass: ^1.53.0
|
||||
svelecte: ^3.13.0
|
||||
svelte: ^3.54.0
|
||||
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-routing: ^1.6.0
|
||||
tslib: ^2.4.1
|
||||
typescript: ^4.9.3
|
||||
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:
|
||||
'@sveltejs/adapter-auto': 1.0.0_@sveltejs+kit@1.0.1
|
||||
'@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/parser': 5.47.1_lzzuuodtsqwxnvqeq4g4likcqa
|
||||
eslint: 8.30.0
|
||||
@ -370,6 +398,19 @@ packages:
|
||||
- supports-color
|
||||
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:
|
||||
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
|
||||
dev: true
|
||||
@ -386,6 +427,10 @@ packages:
|
||||
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
|
||||
dev: true
|
||||
|
||||
/@types/qs/6.9.7:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
dev: false
|
||||
|
||||
/@types/sass/1.43.1:
|
||||
resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==}
|
||||
dependencies:
|
||||
@ -577,6 +622,20 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
@ -611,6 +670,13 @@ packages:
|
||||
streamsearch: 1.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
@ -639,6 +705,10 @@ packages:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/chroma-js/2.4.2:
|
||||
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
|
||||
dev: false
|
||||
|
||||
/color-convert/2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@ -650,6 +720,13 @@ packages:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
dev: true
|
||||
@ -680,6 +757,10 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/dedent-js/1.0.1:
|
||||
resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==}
|
||||
dev: false
|
||||
|
||||
/deep-is/0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
dev: true
|
||||
@ -689,6 +770,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/delayed-stream/1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/detect-indent/6.1.0:
|
||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -959,6 +1045,25 @@ packages:
|
||||
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
@ -973,7 +1078,14 @@ packages:
|
||||
|
||||
/function-bind/1.1.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
@ -1040,12 +1152,16 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/has-symbols/1.0.3:
|
||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: false
|
||||
|
||||
/has/1.0.3:
|
||||
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dependencies:
|
||||
function-bind: 1.1.1
|
||||
dev: true
|
||||
|
||||
/ignore/5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
@ -1119,6 +1235,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-promise/4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
dev: false
|
||||
|
||||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
dev: true
|
||||
@ -1142,6 +1262,10 @@ packages:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
dev: true
|
||||
|
||||
/jwt-decode/3.1.2:
|
||||
resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
|
||||
dev: false
|
||||
|
||||
/kleur/4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
@ -1166,6 +1290,12 @@ packages:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
||||
/lower-case/2.0.2:
|
||||
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
dev: false
|
||||
|
||||
/lru-cache/6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
@ -1199,6 +1329,18 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
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:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@ -1255,11 +1397,22 @@ packages:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/object-inspect/1.12.2:
|
||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||
dev: false
|
||||
|
||||
/once/1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
@ -1299,6 +1452,13 @@ packages:
|
||||
callsites: 3.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1362,11 +1522,22 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/proxy-from-env/1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
|
||||
/punycode/2.1.1:
|
||||
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
|
||||
engines: {node: '>=6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
@ -1480,6 +1651,14 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
|
||||
engines: {node: '>= 10'}
|
||||
@ -1550,6 +1729,12 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
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:
|
||||
resolution: {integrity: sha512-Nt1aWHTOKFReBpmJ1vPug0aGysqPwJh2seM1OvICfM2oeyaA62mOiy5EvkXhltGfhCcIQcq2LoE0l1CwcWPjlw==}
|
||||
hasBin: true
|
||||
@ -1578,6 +1763,12 @@ packages:
|
||||
- sugarss
|
||||
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:
|
||||
resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==}
|
||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||
@ -1587,6 +1778,25 @@ packages:
|
||||
svelte: 3.55.0
|
||||
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:
|
||||
resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==}
|
||||
engines: {node: '>= 9.11.2'}
|
||||
@ -1639,10 +1849,36 @@ packages:
|
||||
typescript: 4.9.4
|
||||
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:
|
||||
resolution: {integrity: sha512-uGu2FVMlOuey4JoKHKrpZFkoYyj0VLjJdz47zX5+gVK5odxHM40RVhar9/iK2YFRVxvfg9FkhfVlR0sjeIrOiA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
@ -1673,7 +1909,6 @@ packages:
|
||||
|
||||
/tslib/2.4.1:
|
||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||
dev: true
|
||||
|
||||
/tsutils/3.21.0_typescript@4.9.4:
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
@ -1701,7 +1936,6 @@ packages:
|
||||
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
|
||||
engines: {node: '>=4.2.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/undici/5.14.0:
|
||||
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 */
|
||||
.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">
|
||||
import type { Exercice } from '../../types/exo.type';
|
||||
import { getContext } from 'svelte';
|
||||
import ModalCard from './ModalCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { cloneExo } from '../../requests/exo.request';
|
||||
import TagContainer from './TagContainer.svelte';
|
||||
import PrivacyIndicator from './PrivacyIndicator.svelte';
|
||||
import MdContentCopy from 'svelte-icons/md/MdContentCopy.svelte';
|
||||
export let exo: Exercice;
|
||||
|
||||
export let title: string;
|
||||
export let examples: Array<string>;
|
||||
export let tags: Array<string>;
|
||||
|
||||
const { show } = getContext<{show: Function}>('modal');
|
||||
const { show } = getContext<{ show: Function }>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
const { isAuth } = getContext<{ isAuth: boolean }>('auth');
|
||||
const exerciceStore = getContext('exos');
|
||||
const tagsStore = getContext('tags');
|
||||
|
||||
let opened = false;
|
||||
const handleClick = () => {
|
||||
console.log('OOOPPP');
|
||||
show(ModalCard, {exo: {title, examples, tags}})
|
||||
opened = true;
|
||||
navigate(`/exercices/${exo.id_code}`);
|
||||
};
|
||||
|
||||
let tg = false;
|
||||
$: !!opened &&
|
||||
show(
|
||||
ModalCard,
|
||||
{
|
||||
exo,
|
||||
exos: exerciceStore,
|
||||
tags: tagsStore
|
||||
},
|
||||
() => {
|
||||
navigate(-1);
|
||||
opened = false;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="card" on:click={handleClick}>
|
||||
<h1>{title}</h1>
|
||||
<div
|
||||
class="card"
|
||||
class:tagMode={tg}
|
||||
on:click={handleClick}
|
||||
on:dblclick={() => {}}
|
||||
on:keypress={() => {}}
|
||||
>
|
||||
<h1>{exo.name}</h1>
|
||||
<div class="examples">
|
||||
<h2>Exemples</h2>
|
||||
{#each examples.slice(0, 3) as ex}
|
||||
<p>{ex}</p>
|
||||
{#if !!exo.consigne}<p>{exo.consigne}</p>{/if}
|
||||
{#each exo.examples.data.slice(0, 3) as ex}
|
||||
<p>{ex.calcul}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="tags"
|
||||
on:click={() => {
|
||||
alert('test');
|
||||
}}
|
||||
/>
|
||||
{#if !!isAuth}
|
||||
{#if exo.is_author && exo.original == null}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={exo.private == true ? 'red' : 'green'}>
|
||||
{exo.private == true ? 'Privé' : 'Public'}</PrivacyIndicator
|
||||
>
|
||||
</div>
|
||||
{:else if !exo.is_author}
|
||||
<div class="status">
|
||||
<PrivacyIndicator color={'blue'}>
|
||||
Par <strong>{exo.author.username}</strong>
|
||||
</PrivacyIndicator>
|
||||
<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" />
|
||||
{#if !!isAuth}
|
||||
<TagContainer bind:exo />
|
||||
{/if}
|
||||
<!-- TagContainer Must be directly after card-hover for the hover effect -->
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
* {
|
||||
transition: 0.45s;
|
||||
}
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transform: scale(0.9);
|
||||
&:hover {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.examples {
|
||||
color: gray;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 20px;
|
||||
p {
|
||||
margin: 10px;
|
||||
margin-left: 18px;
|
||||
font-size: 0.95em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
h2 {
|
||||
font-size: 0.95em;
|
||||
margin: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,9 +148,16 @@
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
border: 1px solid green;
|
||||
border: 1px solid $border;
|
||||
+ :global(div) {
|
||||
transition: 0.45s;
|
||||
}
|
||||
&: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);
|
||||
}
|
||||
@ -66,19 +167,17 @@
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
&:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
max-width: 88%;
|
||||
|
||||
.tags {
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
background-color: blue;
|
||||
right: 1px;
|
||||
left: 1px;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
@ -86,9 +185,10 @@
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: violet;
|
||||
background-color: $background;
|
||||
min-height: 250px;
|
||||
&:hover {
|
||||
max-height: 300px;
|
||||
&:not(.tagMode):hover {
|
||||
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>
|
||||
import { each } from 'svelte/internal';
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte/internal';
|
||||
import Card from './Card.svelte';
|
||||
import Head from './Head.svelte';
|
||||
import ModalCard from './ModalCard.svelte';
|
||||
let exos = [
|
||||
{
|
||||
title: 'test1',
|
||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
title: 'test2',
|
||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
title: 'test3',
|
||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
title: 'test1',
|
||||
examples: ['an example', 'an example', 'an example', 'an example', 'an example'],
|
||||
tags: []
|
||||
import { Query, useQueryClient, type QueryOptions } from '@sveltestack/svelte-query';
|
||||
import { getExo, getExos, getTags } from '../../requests/exo.request';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import Pagination from './Pagination.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
import { setContext } from 'svelte';
|
||||
import type { Page, Tag } from '../../types/exo.type';
|
||||
import type { Store } from '../../types/api.type';
|
||||
|
||||
const { show } = getContext<{ show: Function }>('modal');
|
||||
const { navigate } = getContext<{ navigate: Function }>('navigation');
|
||||
|
||||
let filter = 'user';
|
||||
|
||||
const exerciceStore = writable<Store<Page|undefined>>({
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
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>
|
||||
|
||||
|
||||
{#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="title">
|
||||
<h1>
|
||||
@ -36,9 +94,14 @@
|
||||
publics
|
||||
</p>
|
||||
</div>
|
||||
{#each exos as e}
|
||||
<Card examples={e.examples} title={e.title} tags={e.tags} />
|
||||
{/each}
|
||||
{#if $exerciceStore.data != undefined}
|
||||
{#each $exerciceStore.data.items.filter((e) => e != null && selected.every((t) => e.tags
|
||||
.map((s) => s.id_code)
|
||||
.includes(t.id_code))) as e}
|
||||
<Card bind:exo={e} />
|
||||
{/each}
|
||||
<Pagination bind:page={activePage} total={$exerciceStore.data.totalPage} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -48,6 +111,7 @@
|
||||
grid-auto-flow: dense;
|
||||
grid-gap: 32px;
|
||||
margin: 0 auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.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>
|
||||
export let exo;
|
||||
let examples = ['an example', 'an example', 'an example', 'an example', 'an example'];
|
||||
<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, 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>
|
||||
|
||||
<div class="modal">
|
||||
<h1>Titre</h1>
|
||||
<input type="text" class="input" />
|
||||
<div class="examples">
|
||||
<h2>Exemples</h2>
|
||||
{#each examples as e}
|
||||
<p>{e}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<button>Télécharger</button>
|
||||
<div class="tags" />
|
||||
{#if editing === false}
|
||||
<DownloadForm
|
||||
{exo}
|
||||
delete_={() => {
|
||||
exos.update((o) => {
|
||||
return {
|
||||
...o,
|
||||
data: { ...o.data, items: [...o.data.items.filter((e) => e.id_code != exo.id_code)] }
|
||||
};
|
||||
});
|
||||
}}
|
||||
edit={() => {
|
||||
editing = true;
|
||||
}}
|
||||
/>
|
||||
{:else if editing === true}
|
||||
<EditForm
|
||||
bind:exo
|
||||
cancel={() => {
|
||||
editing = false;
|
||||
}}
|
||||
{updateExo}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../variables';
|
||||
.modal {
|
||||
min-width: 820px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(5, auto);
|
||||
background: blue;
|
||||
padding:70px;
|
||||
grid-gap:10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.examples {
|
||||
h2 {
|
||||
font-size: 1em;
|
||||
font-weight: 800;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
p {
|
||||
margin: 5px 0;
|
||||
margin-left: 30px;
|
||||
}
|
||||
background: $background;
|
||||
padding: 70px;
|
||||
grid-gap: 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
</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">
|
||||
import { onDestroy, setContext, SvelteComponent } from 'svelte';
|
||||
import { onDestroy, setContext, SvelteComponent } from 'svelte';
|
||||
|
||||
let visible = false;
|
||||
let onClose: Function;
|
||||
let props = {};
|
||||
let component: ConstructorOfATypedSvelteComponent | undefined;
|
||||
function show(c : ConstructorOfATypedSvelteComponent, p: Object) {
|
||||
visible = true
|
||||
component = c
|
||||
props = p
|
||||
}
|
||||
function close(){
|
||||
visible=false
|
||||
component = undefined;
|
||||
props = {}
|
||||
onClose()
|
||||
}
|
||||
setContext('modal', {show, close})
|
||||
|
||||
let props = {};
|
||||
let component: ConstructorOfATypedSvelteComponent | undefined;
|
||||
let closed = true;
|
||||
let onClose: Function = () => {};
|
||||
function show(
|
||||
c: ConstructorOfATypedSvelteComponent,
|
||||
p: Object,
|
||||
newOnClose: Function = () => {},
|
||||
editing: boolean = false
|
||||
) {
|
||||
if (editing == false && closed === false) return;
|
||||
visible = true;
|
||||
closed = false;
|
||||
component = c;
|
||||
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) {
|
||||
console.log('HOP');
|
||||
if (e.key == 'Escape' && visible == true) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<div id="topModal" class:visible on:click={() => close()} on:keypress={keyPress}>
|
||||
<div id="modal" on:click|stopPropagation={()=>{}} on:keypress={()=>{}}>
|
||||
<svelte:component this={component} {...props}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overlay"
|
||||
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>
|
||||
|
||||
|
||||
<style>
|
||||
#topModal {
|
||||
visibility: hidden;
|
||||
z-index: 9999;
|
||||
<style lang="scss">
|
||||
#modal {
|
||||
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;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #4448;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
visibility: hidden;
|
||||
transition: 0.3s;
|
||||
&.visible {
|
||||
opacity: 0.7;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
#modal {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
border: 2px solid #000;
|
||||
filter: drop-shadow(5px 5px 5px #555);
|
||||
padding: 1em;
|
||||
.closing {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* #modal-content {
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: calc(100vh - 20px);
|
||||
overflow: auto;
|
||||
} */
|
||||
</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 NavLink from '../components/NavLink.svelte';
|
||||
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>
|
||||
|
||||
<Modal>
|
||||
<main>
|
||||
<nav data-sveltekit-preload-data="hover">
|
||||
<NavLink href="/" exact>Home</NavLink>
|
||||
<NavLink href="/exercices" exact>Exercices</NavLink>
|
||||
<NavLink href="/settings" exact>Settings</NavLink>
|
||||
</nav>
|
||||
<slot />
|
||||
</main>
|
||||
</Modal>
|
||||
<Navigation>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Auth>
|
||||
<Alert>
|
||||
<Modal>
|
||||
<main>
|
||||
<nav data-sveltekit-preload-data="hover">
|
||||
<NavLink href="/" exact>Home</NavLink>
|
||||
<NavLink href="/exercices" exact>Exercices</NavLink>
|
||||
<NavLink href="/settings" exact>Settings</NavLink>
|
||||
</nav>
|
||||
<slot />
|
||||
</main>
|
||||
</Modal>
|
||||
</Alert>
|
||||
</Auth>
|
||||
</QueryClientProvider>
|
||||
</Navigation>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../variables';
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -41,9 +56,9 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
overflow: hidden;
|
||||
background-color: #1d1a5a;
|
||||
background-color: $background;
|
||||
color: #d9d9d9;
|
||||
background: linear-gradient(to bottom left, #0d0221 30%, #1a0f7a);
|
||||
background: linear-gradient(to bottom left, $background-dark 30%, $background-light);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
@ -54,6 +69,7 @@
|
||||
padding-right: calc(50% - var(--container-width) / 2);
|
||||
height: calc(100vh - var(--navbar-height) - 10px);
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
@ -65,7 +81,7 @@
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 30px 0;
|
||||
border-bottom: 1px solid #181553;
|
||||
border-bottom: 1px solid $border;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
height: 30px;
|
||||
|
@ -1,16 +1,15 @@
|
||||
<script>
|
||||
import Card from '../components/exos/Card.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
let count = 1;
|
||||
const { show } = getContext('modal');
|
||||
|
||||
const add = () => {
|
||||
show(Card, { title: 'test', examples: ['test'], tags: [], click: () => {} });
|
||||
};
|
||||
import { getExos } from '../requests/exo.request';
|
||||
import { useQuery } from '@sveltestack/svelte-query';
|
||||
import { browser } from '$app/environment'
|
||||
const t = useQuery('tst',async ()=>{
|
||||
return getExos('public' , {})
|
||||
});
|
||||
$: console.log("DATA", $t.data)
|
||||
</script>
|
||||
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<div>
|
||||
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation
|
||||
<button on:click={add}>test {count}</button>
|
||||
</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
console.log(getExos('public', {}));
|
||||
}}>test</button
|
||||
>
|
||||
|
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} */
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
plugins: [sveltekit()],
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: "@use \"src/variables.scss\" as *;"
|
||||
}
|
||||
}
|
||||
}
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "src/variables.scss" as *;'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
optimizeDeps: { exclude: ['svelte-navigator'] }
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -18,7 +18,7 @@
|
||||
transition: transform .3s;
|
||||
}
|
||||
}
|
||||
} */
|
||||
} */
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin-top: 10px;
|
||||
|
Loading…
Reference in New Issue
Block a user