before style

This commit is contained in:
Kilton937342 2023-01-27 21:41:08 +01:00
parent ccd047dbb0
commit 43031d22fb
44 changed files with 2730 additions and 294 deletions

View File

@ -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()}})

View File

@ -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
View 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")

View File

@ -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)

View File

@ -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'])

View 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)

View File

@ -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'"}}

View File

@ -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"
}
}

View File

@ -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==}

View 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')}` })
}
});

View File

@ -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;
}

View File

@ -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);
}
}

View 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>

View 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>

View 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>

View File

@ -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 {

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 />

View File

@ -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>

View 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 />

View 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;
});
}

View 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();
});
};

View File

@ -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;

View File

@ -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
>

View 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>

View File

@ -0,0 +1,7 @@
export type Store<T> = {
isLoading: boolean,
isFetching: boolean,
isSuccess: boolean,
data: T
}

View 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
}

View 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' };
};
};

View 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;
};

View File

@ -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;

View File

@ -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;

View File

@ -18,7 +18,7 @@
transition: transform .3s;
}
}
} */
} */
.input-container {
position: relative;
margin-top: 10px;