Merge pull request #57 from mawoka-myblock/dev
This commit is contained in:
+11
-8
@@ -6,7 +6,7 @@ from starlette.middleware.sessions import SessionMiddleware
|
|||||||
|
|
||||||
from classquiz.config import settings
|
from classquiz.config import settings
|
||||||
from classquiz.db import database
|
from classquiz.db import database
|
||||||
from classquiz.routers import users, quiz, utils, stats, storage, search, testing_routes
|
from classquiz.routers import users, quiz, utils, stats, storage, search, testing_routes, editor
|
||||||
from classquiz.socket_server import sio
|
from classquiz.socket_server import sio
|
||||||
from classquiz.helpers import meilisearch_init
|
from classquiz.helpers import meilisearch_init
|
||||||
|
|
||||||
@@ -45,11 +45,14 @@ async def shutdown() -> None:
|
|||||||
|
|
||||||
|
|
||||||
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
||||||
app.include_router(users.router, tags=["users"], prefix="/api/v1/users")
|
app.include_router(users.router, tags=["users"], prefix="/api/v1/users", include_in_schema=False)
|
||||||
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz")
|
app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz", include_in_schema=False)
|
||||||
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils")
|
app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils", include_in_schema=True)
|
||||||
app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats")
|
app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats", include_in_schema=True)
|
||||||
app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage")
|
app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage", include_in_schema=True)
|
||||||
app.include_router(search.router, tags=["search"], prefix="/api/v1/search")
|
app.include_router(search.router, tags=["search"], prefix="/api/v1/search", include_in_schema=True)
|
||||||
app.include_router(testing_routes.router, tags=["internal", "testing"], prefix="/api/v1/internal/testing")
|
app.include_router(
|
||||||
|
testing_routes.router, tags=["internal", "testing"], prefix="/api/v1/internal/testing", include_in_schema=False
|
||||||
|
)
|
||||||
|
app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True)
|
||||||
app.mount("/", ASGIApp(sio))
|
app.mount("/", ASGIApp(sio))
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
import redis.asyncio as redis_lib
|
from redis import asyncio as redis_lib
|
||||||
|
import redis as redis_base_lib
|
||||||
from pydantic import BaseSettings, RedisDsn, PostgresDsn
|
from pydantic import BaseSettings, RedisDsn, PostgresDsn
|
||||||
import meilisearch as MeiliSearch
|
import meilisearch as MeiliSearch
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -53,7 +54,7 @@ def settings() -> Settings:
|
|||||||
return Settings()
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
redis: redis_lib.client.Redis = redis_lib.Redis().from_url(settings().redis)
|
redis: redis_base_lib.client.Redis = redis_lib.Redis().from_url(settings().redis)
|
||||||
storage: Storage = Storage(
|
storage: Storage = Storage(
|
||||||
backend=settings().storage_backend,
|
backend=settings().storage_backend,
|
||||||
deta_key=settings().deta_project_key,
|
deta_key=settings().deta_project_key,
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import asyncio
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import asyncpg.exceptions
|
||||||
|
import bleach
|
||||||
|
from fastapi import APIRouter, File, UploadFile, HTTPException, Depends
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from classquiz.config import settings, redis, storage, meilisearch
|
||||||
|
from classquiz.db.models import Quiz, QuizInput, User
|
||||||
|
from classquiz.auth import get_current_user
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from classquiz.helpers import get_meili_data
|
||||||
|
from classquiz.storage.errors import DeletionFailedError
|
||||||
|
|
||||||
|
settings = settings()
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class InitEditorResponse(BaseModel):
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class EditSessionData(BaseModel):
|
||||||
|
quiz_id: UUID
|
||||||
|
edit: bool
|
||||||
|
user_id: UUID
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_images_for_edit_id(edit_id: str):
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
res = await redis.lrange(f"edit_session:{edit_id}:images", 0, -1)
|
||||||
|
if len(res) != 0:
|
||||||
|
for image_id in res:
|
||||||
|
await storage.delete([image_id])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start", response_model=InitEditorResponse)
|
||||||
|
async def init_editor(edit: bool, quiz_id: Optional[UUID] = None, user: User = Depends(get_current_user)):
|
||||||
|
if edit and quiz_id is not None and await Quiz.objects.get_or_none(id=quiz_id, user_id=user.id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Quiz not found")
|
||||||
|
if not edit and quiz_id is not None:
|
||||||
|
raise HTTPException(status_code=400, detail="You can't choose the id for your quiz")
|
||||||
|
if edit and quiz_id is None:
|
||||||
|
raise HTTPException(status_code=400, detail="Edit can't be true if quiz_id is None")
|
||||||
|
if quiz_id is None:
|
||||||
|
quiz_id = uuid.uuid4()
|
||||||
|
edit_id = os.urandom(4).hex()
|
||||||
|
await redis.sadd("edit_sessions", edit_id)
|
||||||
|
await redis.set(
|
||||||
|
f"edit_session:{edit_id}", EditSessionData(quiz_id=quiz_id, edit=edit, user_id=user.id).json(), ex=3600
|
||||||
|
)
|
||||||
|
return InitEditorResponse(token=edit_id)
|
||||||
|
|
||||||
|
|
||||||
|
class UploadImageReturn(BaseModel):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/image", response_model=UploadImageReturn)
|
||||||
|
async def upload_image(edit_id: str, file: UploadFile = File()):
|
||||||
|
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||||
|
if session_data is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
|
session_data = EditSessionData.parse_raw(session_data)
|
||||||
|
file_name = f"{session_data.quiz_id}--{uuid.uuid4()}"
|
||||||
|
print("Uploading...")
|
||||||
|
await storage.upload(file_name=file_name, file_data=await file.read())
|
||||||
|
print("Finished Upload")
|
||||||
|
await redis.lpush(f"edit_session:{edit_id}:images", file_name)
|
||||||
|
return UploadImageReturn(id=file_name)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/finish")
|
||||||
|
async def finish_edit(edit_id: str, quiz_input: QuizInput):
|
||||||
|
session_data = await redis.get(f"edit_session:{edit_id}")
|
||||||
|
if session_data is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Edit ID not found!")
|
||||||
|
session_data = EditSessionData.parse_raw(session_data)
|
||||||
|
quiz_input.title = html.unescape(bleach.clean(quiz_input.title, tags=[], strip=True))
|
||||||
|
quiz_input.description = html.unescape(bleach.clean(quiz_input.description, tags=[], strip=True))
|
||||||
|
image_id_regex = r"^.{36}--.{36}$"
|
||||||
|
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||||
|
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
||||||
|
extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$"
|
||||||
|
images_to_delete = []
|
||||||
|
old_quiz_data = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id)
|
||||||
|
|
||||||
|
def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None):
|
||||||
|
if old_quiz is None:
|
||||||
|
return
|
||||||
|
print(new, old_quiz.questions[index]["image"])
|
||||||
|
if new == old_quiz.questions[index]["image"]:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
images_to_delete.append(old_quiz.questions[index]["image"])
|
||||||
|
|
||||||
|
for i, question in enumerate(quiz_input.questions):
|
||||||
|
image = question.image
|
||||||
|
if image == "":
|
||||||
|
question.image = None
|
||||||
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
|
elif image is None:
|
||||||
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
|
elif bool(re.match(image_id_regex, question.image)):
|
||||||
|
question.image = f"{settings.root_address}/api/v1/storage/download/{image}"
|
||||||
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
|
elif bool(re.match(imgur_regex, image)):
|
||||||
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
|
elif bool(re.match(server_regex, image)):
|
||||||
|
mark_image_for_deletion(question.image, i, old_quiz_data)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Image URL(s) aren't valid!")
|
||||||
|
print(images_to_delete)
|
||||||
|
if session_data.edit:
|
||||||
|
quiz = old_quiz_data
|
||||||
|
meilisearch.index(settings.meilisearch_index).update_documents([await get_meili_data(quiz)])
|
||||||
|
if quiz.public and not quiz_input.public:
|
||||||
|
meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
|
||||||
|
if not quiz.public and quiz_input.public:
|
||||||
|
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
|
||||||
|
quiz.title = quiz_input.title
|
||||||
|
quiz.public = quiz_input.public
|
||||||
|
quiz.description = quiz_input.description
|
||||||
|
quiz.updated_at = datetime.now()
|
||||||
|
quiz.questions = quiz_input.dict()["questions"]
|
||||||
|
for image in images_to_delete:
|
||||||
|
if image is not None:
|
||||||
|
try:
|
||||||
|
await storage.delete([re.search(extract_file_name_re, image).group(1)])
|
||||||
|
except DeletionFailedError:
|
||||||
|
pass
|
||||||
|
await redis.srem("edit_sessions", edit_id)
|
||||||
|
await redis.delete(f"edit_session:{edit_id}")
|
||||||
|
await redis.delete(f"edit_session:{edit_id}:images")
|
||||||
|
return await quiz.update()
|
||||||
|
else:
|
||||||
|
quiz = Quiz(**quiz_input.dict(), user_id=session_data.user_id, id=session_data.quiz_id)
|
||||||
|
await redis.delete("global_quiz_count")
|
||||||
|
if quiz_input.public:
|
||||||
|
meilisearch.index(settings.meilisearch_index).add_documents([await get_meili_data(quiz)])
|
||||||
|
try:
|
||||||
|
await redis.srem("edit_sessions", edit_id)
|
||||||
|
await redis.delete(f"edit_session:{edit_id}")
|
||||||
|
await redis.delete(f"edit_session:{edit_id}:images")
|
||||||
|
return await quiz.save()
|
||||||
|
except asyncpg.exceptions.UniqueViolationError:
|
||||||
|
raise HTTPException(status_code=400, detail="The quiz already exists")
|
||||||
@@ -20,7 +20,7 @@ settings = settings()
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
@router.post("/create", deprecated=True)
|
||||||
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
||||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||||
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{36}--.{36}$"
|
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{36}--.{36}$"
|
||||||
@@ -129,7 +129,7 @@ async def get_quiz_list(user: User = Depends(get_current_user)):
|
|||||||
return await Quiz.objects.filter(user_id=user.id).all()
|
return await Quiz.objects.filter(user_id=user.id).all()
|
||||||
|
|
||||||
|
|
||||||
@router.put("/update/{quiz_id}")
|
@router.put("/update/{quiz_id}", deprecated=True)
|
||||||
async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends(get_current_user)):
|
||||||
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$"
|
||||||
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from classquiz.storage.errors import DeletionFailedError, SavingFailedError, DownloadingFailedError
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ class DetaStorage:
|
|||||||
elif response.status == 404:
|
elif response.status == 404:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise Exception("Download failed")
|
raise DownloadingFailedError
|
||||||
|
|
||||||
async def upload(self, file: bytes, file_name: str) -> None:
|
async def upload(self, file: bytes, file_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -40,7 +41,7 @@ class DetaStorage:
|
|||||||
if response.status == 201:
|
if response.status == 201:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise Exception("Upload failed")
|
raise SavingFailedError
|
||||||
|
|
||||||
async def delete(self, file_names: [str]) -> None:
|
async def delete(self, file_names: [str]) -> None:
|
||||||
async with ClientSession(headers=self.headers) as session, session.delete(
|
async with ClientSession(headers=self.headers) as session, session.delete(
|
||||||
@@ -49,4 +50,4 @@ class DetaStorage:
|
|||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise Exception("Delete failed")
|
raise DeletionFailedError
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class DeletionFailedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SavingFailedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadingFailedError(Exception):
|
||||||
|
pass
|
||||||
@@ -31,6 +31,13 @@
|
|||||||
"@types/yup": "^0.29.14",
|
"@types/yup": "^0.29.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
||||||
"@typescript-eslint/parser": "^5.27.1",
|
"@typescript-eslint/parser": "^5.27.1",
|
||||||
|
"@uppy/compressor": "^0.3.0",
|
||||||
|
"@uppy/core": "^2.3.1",
|
||||||
|
"@uppy/dashboard": "^2.3.0",
|
||||||
|
"@uppy/drop-target": "^1.1.3",
|
||||||
|
"@uppy/image-editor": "^1.3.0",
|
||||||
|
"@uppy/svelte": "^1.0.8",
|
||||||
|
"@uppy/xhr-upload": "^2.1.2",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cssnano": "^5.1.11",
|
"cssnano": "^5.1.11",
|
||||||
|
|||||||
Generated
+372
-3
@@ -18,6 +18,13 @@ specifiers:
|
|||||||
'@types/yup': ^0.29.14
|
'@types/yup': ^0.29.14
|
||||||
'@typescript-eslint/eslint-plugin': ^5.27.1
|
'@typescript-eslint/eslint-plugin': ^5.27.1
|
||||||
'@typescript-eslint/parser': ^5.27.1
|
'@typescript-eslint/parser': ^5.27.1
|
||||||
|
'@uppy/compressor': ^0.3.0
|
||||||
|
'@uppy/core': ^2.3.1
|
||||||
|
'@uppy/dashboard': ^2.3.0
|
||||||
|
'@uppy/drop-target': ^1.1.3
|
||||||
|
'@uppy/image-editor': ^1.3.0
|
||||||
|
'@uppy/svelte': ^1.0.8
|
||||||
|
'@uppy/xhr-upload': ^2.1.2
|
||||||
autoprefixer: ^10.4.7
|
autoprefixer: ^10.4.7
|
||||||
cookie: ^0.5.0
|
cookie: ^0.5.0
|
||||||
cssnano: ^5.1.11
|
cssnano: ^5.1.11
|
||||||
@@ -73,6 +80,13 @@ devDependencies:
|
|||||||
'@types/yup': 0.29.14
|
'@types/yup': 0.29.14
|
||||||
'@typescript-eslint/eslint-plugin': 5.27.1_aq7uryhocdbvbqum33pitcm3y4
|
'@typescript-eslint/eslint-plugin': 5.27.1_aq7uryhocdbvbqum33pitcm3y4
|
||||||
'@typescript-eslint/parser': 5.27.1_ud6rd4xtew5bv4yhvkvu24pzm4
|
'@typescript-eslint/parser': 5.27.1_ud6rd4xtew5bv4yhvkvu24pzm4
|
||||||
|
'@uppy/compressor': 0.3.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/dashboard': 2.3.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/drop-target': 1.1.3_@uppy+core@2.3.1
|
||||||
|
'@uppy/image-editor': 1.3.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/svelte': 1.0.8_w57owxejta2mqj4rr2tj54bhsi
|
||||||
|
'@uppy/xhr-upload': 2.1.2_@uppy+core@2.3.1
|
||||||
autoprefixer: 10.4.7_postcss@8.4.14
|
autoprefixer: 10.4.7_postcss@8.4.14
|
||||||
cookie: 0.5.0
|
cookie: 0.5.0
|
||||||
cssnano: 5.1.11_postcss@8.4.14
|
cssnano: 5.1.11_postcss@8.4.14
|
||||||
@@ -592,6 +606,20 @@ packages:
|
|||||||
tailwindcss: 3.1.2
|
tailwindcss: 3.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@transloadit/prettier-bytes/0.0.7:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@transloadit/prettier-bytes/0.0.9:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@trysound/sax/0.2.0:
|
/@trysound/sax/0.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -822,6 +850,226 @@ packages:
|
|||||||
eslint-visitor-keys: 3.3.0
|
eslint-visitor-keys: 3.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/companion-client/2.2.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-Y3E10NJLMfp/wjgthNhx3gJtT67fzFCPNPFwpNNRs5iJsW6PANhJ420eyMUFzfmEZ56ZzGYxr5pzJZx8YxHICQ==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
namespace-emitter: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/compressor/0.3.0_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-M05fz0TA5Oy0wW930j+eROlcPADNu20S0L2n4ej7bZlUzNLMixjE6UnwW5huSm2vzZV5tEPKvSJ0SnU/E+sM4Q==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@transloadit/prettier-bytes': 0.0.9
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
compressorjs: 1.1.1
|
||||||
|
preact: 10.8.1
|
||||||
|
promise-queue: 2.2.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/core/2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-KV04X7ueYbYX1p37/i3QsoQSw8IDP8Yb+Bh9KNN0X2Vcun6K2VnNjhVtPmPXtyjDZooK7lVIqhRX8TZWcSfgSQ==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
'@transloadit/prettier-bytes': 0.0.7
|
||||||
|
'@uppy/store-default': 2.1.0
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
lodash.throttle: 4.1.1
|
||||||
|
mime-match: 1.0.2
|
||||||
|
namespace-emitter: 2.0.1
|
||||||
|
nanoid: 3.3.4
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/dashboard/2.3.0_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-q7GaLrYt3Tb+x4R8rARfzBuqPtdf6lKnc5Rg/XW18VOIaQM/UZNuBr+EVGhwuDiQt/1QJz1t2lS2Mx246zJ/qA==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@transloadit/prettier-bytes': 0.0.7
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/informer': 2.1.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/provider-views': 2.1.1_@uppy+core@2.3.1
|
||||||
|
'@uppy/status-bar': 2.2.1_@uppy+core@2.3.1
|
||||||
|
'@uppy/thumbnail-generator': 2.2.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
classnames: 2.3.1
|
||||||
|
is-shallow-equal: 1.0.1
|
||||||
|
lodash.debounce: 4.0.8
|
||||||
|
memoize-one: 5.2.1
|
||||||
|
nanoid: 3.3.4
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/drag-drop/2.1.1_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-3cTXzmoMpCJvaM0H59acm/CNkS3y6jr03SYK7u6X/vd3/a6e3XyfaGc/jwKSsGBm+oMLftFKv/inhQLKOZR5CQ==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/drop-target/1.1.3_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-Cd+L3mdVovS6G0Yv0Hah18Utz3czQ75YrGJ65MlHFzfJccOQJczvxn18FVssKyO6Zc0ce1y0mGKGjUClTM5dMA==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.1.9
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/image-editor/1.3.0_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-ZTF0A2e39K5ZDBFu6pSxKPL9Tx6Xslsu0k+1eW2bu2pzX0k4b4hVV6wciBZTr2UzitzdF7x83oIefiVQyBZ7/A==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
cropperjs: 1.5.7
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/informer/2.1.0_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-/nR7Dxh1RH1FvKRPQ/xZk1lS3F+lh93xaN6Z9utGg5u1hHREuaHfFTcEXhkeq1/hiKbVYsjNaNZ5aPe7+4flkg==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/progress-bar/2.1.1_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-Kh/kM5rGUABxwwDRzR3pjDzkNRELES2T3nIBdstRIfWCxlHjIFpgqOkvvNRXKflm2hcYUzTRELgtfi09LuqE9A==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/provider-views/2.1.1_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-Emk18dBAUx73uhkYL/ryFqu9pQ5hVnW5P+b9J/bq3bxDaEtVfQvhFPfaa8/gHNb8t499ytHmRPJYUJlqfG3/5A==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
classnames: 2.3.1
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/status-bar/2.2.1_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-jnJ/DSEuWGPU6+m0i2yfGl5cPb6CJnxDtcL6/gXKOyjGeRfriWjeEsRHmEkdK+xLv/AJC//eEfSOCL3N737p0g==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@transloadit/prettier-bytes': 0.0.7
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
classnames: 2.3.1
|
||||||
|
lodash.throttle: 4.1.1
|
||||||
|
preact: 10.8.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/store-default/2.1.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-BkcR1wGw6Kwbvr8m1tKF9EDDWSTJoTGnVseBF/iW4bzR22assbtxZIE1iroo68UMqYEG4rv63SX4BUEtNvVjdA==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/svelte/1.0.8_w57owxejta2mqj4rr2tj54bhsi:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-A/80S2LCRjszzGbdDe7e9YU77HAa2PiIMqAjmyuOY5ZMyoIsRjeT8G9CNjeDMGMJBsQLyysDWbF3AZqPyJukQA==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.2.0
|
||||||
|
svelte: ^3.0.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/dashboard': 2.3.0_@uppy+core@2.3.1
|
||||||
|
'@uppy/drag-drop': 2.1.1_@uppy+core@2.3.1
|
||||||
|
'@uppy/progress-bar': 2.1.1_@uppy+core@2.3.1
|
||||||
|
'@uppy/status-bar': 2.2.1_@uppy+core@2.3.1
|
||||||
|
svelte: 3.48.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/thumbnail-generator/2.2.0_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-ySZoB0tK7sQ/PRSFvU5JLxcq/wVqJM/G95cVdJ2Wv1BzQ7+/wKBEPFgJuHDqgzpgUrHwmc+N0JYVXqfk297DZA==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.0
|
||||||
|
dependencies:
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
exifr: 7.1.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/utils/4.1.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-C47DUl4uLzmQZdW+VmetIgGRurXuPsvb+/pyYqh9DJn0Phep8u7AOj/tlJA5CHv4pefNHsFjXpaWfSUG3HtW3A==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
lodash.throttle: 4.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@uppy/xhr-upload/2.1.2_@uppy+core@2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-VCsb7J5yHsof49nnUa+Y1n27UMtqHPttQmmoCa5hmjqa9R7ZISpBkXKOQmZo526eopKNuAKSAdkHWfCm8efJTA==
|
||||||
|
}
|
||||||
|
peerDependencies:
|
||||||
|
'@uppy/core': ^2.3.1
|
||||||
|
dependencies:
|
||||||
|
'@uppy/companion-client': 2.2.1
|
||||||
|
'@uppy/core': 2.3.1
|
||||||
|
'@uppy/utils': 4.1.0
|
||||||
|
nanoid: 3.3.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@vercel/nft/0.19.1:
|
/@vercel/nft/0.19.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -1061,6 +1309,13 @@ packages:
|
|||||||
file-uri-to-path: 1.0.0
|
file-uri-to-path: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/blueimp-canvas-to-blob/3.29.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/boolbase/1.0.0:
|
/boolbase/1.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -1189,6 +1444,13 @@ packages:
|
|||||||
engines: { node: '>=10' }
|
engines: { node: '>=10' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/classnames/2.3.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cluster-key-slot/1.1.0:
|
/cluster-key-slot/1.1.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -1245,6 +1507,16 @@ packages:
|
|||||||
engines: { node: '>= 10' }
|
engines: { node: '>= 10' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/compressorjs/1.1.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-SysRuUPfmUNoq+RviE0iMFVUmoX2q/x+7PkEPUmk6NGkd85hDrmvujx0Qtp8UCGA6KMe5kuodsylPQcNaLf60w==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
blueimp-canvas-to-blob: 3.29.0
|
||||||
|
is-blob: 2.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/concat-map/0.0.1:
|
/concat-map/0.0.1:
|
||||||
resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= }
|
resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= }
|
||||||
dev: true
|
dev: true
|
||||||
@@ -1271,6 +1543,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cropperjs/1.5.7:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-sGj+G/ofKh+f6A4BtXLJwtcKJgMUsXYVUubfTo9grERiDGXncttefmue/fyQFvn8wfdyoD1KhDRYLfjkJFl0yw==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cross-spawn/7.0.3:
|
/cross-spawn/7.0.3:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2170,6 +2449,13 @@ packages:
|
|||||||
engines: { node: '>=0.10.0' }
|
engines: { node: '>=0.10.0' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/exifr/7.1.3:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fast-deep-equal/3.1.3:
|
/fast-deep-equal/3.1.3:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2672,6 +2958,14 @@ packages:
|
|||||||
binary-extensions: 2.2.0
|
binary-extensions: 2.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-blob/2.1.0:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==
|
||||||
|
}
|
||||||
|
engines: { node: '>=6' }
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-core-module/2.9.0:
|
/is-core-module/2.9.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2725,6 +3019,13 @@ packages:
|
|||||||
engines: { node: '>=0.12.0' }
|
engines: { node: '>=0.12.0' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-shallow-equal/1.0.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-lq5RvK+85Hs5J3p4oA4256M1FEffzmI533ikeDHvJd42nouRRx5wBzt36JuviiGe5dIPyHON/d0/Up+PBo6XkQ==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/isarray/1.0.0:
|
/isarray/1.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2819,6 +3120,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.debounce/4.0.8:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lodash.defaults/4.2.0:
|
/lodash.defaults/4.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2854,6 +3162,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.throttle/4.1.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lodash.uniq/4.5.0:
|
/lodash.uniq/4.5.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2953,6 +3268,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/memoize-one/5.2.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/merge2/1.4.1:
|
/merge2/1.4.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -2972,6 +3294,15 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mime-match/1.0.2:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==
|
||||||
|
}
|
||||||
|
dependencies:
|
||||||
|
wildcard: 1.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/min-indent/1.0.1:
|
/min-indent/1.0.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -3092,6 +3423,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/namespace-emitter/2.0.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/nanoclone/0.2.1:
|
/nanoclone/0.2.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -3938,6 +4276,13 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/preact/10.8.1:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-p5CKQ0MCEXTGKGOHiFaNE2V2nDq2hvDHykXvIlz+4lbfJ9umLZr8JS/fa1bXUwRcHXK+Ljk8zqmDhr25n0LtVg==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/prelude-ls/1.2.1:
|
/prelude-ls/1.2.1:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -3975,6 +4320,14 @@ packages:
|
|||||||
}
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/promise-queue/2.2.5:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-p/iXrPSVfnqPft24ZdNNLECw/UrtLTpT3jpAAMzl/o5/rDsGCPo3/CQS2611flL6LkoEJ3oQZw7C8Q80ZISXRQ==
|
||||||
|
}
|
||||||
|
engines: { node: '>= 0.8.0' }
|
||||||
|
dev: true
|
||||||
|
|
||||||
/property-expr/2.0.5:
|
/property-expr/2.0.5:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -4979,11 +5332,17 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/webidl-conversions/3.0.1:
|
/webidl-conversions/3.0.1:
|
||||||
resolution: { integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= }
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||||
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/whatwg-url/5.0.0:
|
/whatwg-url/5.0.0:
|
||||||
resolution: { integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0= }
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||||
|
}
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
@@ -5009,6 +5368,13 @@ packages:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/wildcard/1.1.2:
|
||||||
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==
|
||||||
|
}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/word-wrap/1.2.3:
|
/word-wrap/1.2.3:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@@ -5029,7 +5395,10 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/wrappy/1.0.2:
|
/wrappy/1.0.2:
|
||||||
resolution: { integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= }
|
resolution:
|
||||||
|
{
|
||||||
|
integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||||
|
}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ws/8.2.3:
|
/ws/8.2.3:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
*.png
|
||||||
|
*.xcf
|
||||||
|
admin_overview.webp
|
||||||
|
results.webp
|
||||||
|
select.webp
|
||||||
|
solution.webp
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/** Dispatch event on click outside of node */
|
||||||
|
export function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||||
|
node.dispatchEvent(new CustomEvent('click_outside', node));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClick, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
document.removeEventListener('click', handleClick, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+108
-199
@@ -2,33 +2,21 @@
|
|||||||
import { getLocalization } from '$lib/i18n';
|
import { getLocalization } from '$lib/i18n';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import { dataSchema } from '$lib/yupSchemas';
|
import { dataSchema } from '$lib/yupSchemas';
|
||||||
|
import type { EditorData, Question, Answer } from './quiz_types';
|
||||||
|
import Sidebar from '$lib/editor/sidebar.svelte';
|
||||||
|
import SettingsCard from '$lib/editor/settings-card.svelte';
|
||||||
|
import QuizCard from '$lib/editor/card.svelte';
|
||||||
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
let schemaInvalid = false;
|
let schemaInvalid = false;
|
||||||
let yupErrorMessage = '';
|
let yupErrorMessage = '';
|
||||||
|
|
||||||
interface Data {
|
export let data: EditorData;
|
||||||
public: boolean;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
questions: Question[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Question {
|
|
||||||
question: string;
|
|
||||||
time: string;
|
|
||||||
image?: string;
|
|
||||||
answers: Answer[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Answer {
|
|
||||||
right: boolean;
|
|
||||||
answer: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let data: Data;
|
|
||||||
export let submit_button_text = 'Create';
|
export let submit_button_text = 'Create';
|
||||||
|
export let quiz_id: string | null;
|
||||||
|
let selected_question = -1;
|
||||||
let imgur_links_valid = false;
|
let imgur_links_valid = false;
|
||||||
|
|
||||||
const empty_question: Question = {
|
const empty_question: Question = {
|
||||||
@@ -43,7 +31,7 @@
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateInput = async (data: Data) => {
|
const validateInput = async (data: EditorData) => {
|
||||||
try {
|
try {
|
||||||
await dataSchema.validate(data, { abortEarly: false });
|
await dataSchema.validate(data, { abortEarly: false });
|
||||||
schemaInvalid = false;
|
schemaInvalid = false;
|
||||||
@@ -77,191 +65,112 @@
|
|||||||
right: false,
|
right: false,
|
||||||
answer: ''
|
answer: ''
|
||||||
};
|
};
|
||||||
|
let edit_id;
|
||||||
|
let confirm_to_leave = true;
|
||||||
|
|
||||||
|
const getEditID = async () => {
|
||||||
|
let res;
|
||||||
|
if (quiz_id === null) {
|
||||||
|
res = await fetch(`/api/v1/editor/start?edit=false`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await fetch(`/api/v1/editor/start?edit=true&quiz_id=${quiz_id}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = await res.json();
|
||||||
|
edit_id = json.token;
|
||||||
|
console.log(edit_id, json);
|
||||||
|
} else {
|
||||||
|
alert('Error!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmUnload = (event) => {
|
||||||
|
console.log(confirm_to_leave);
|
||||||
|
if (!confirm_to_leave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = 'Are you sure you want to leave?';
|
||||||
|
localStorage.setItem('edit_game', JSON.stringify(data));
|
||||||
|
return 'unload';
|
||||||
|
};
|
||||||
|
const saveQuiz = async () => {
|
||||||
|
if (schemaInvalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await fetch(`/api/v1/editor/finish?edit_id=${edit_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
console.log('Hier');
|
||||||
|
confirm_to_leave = false;
|
||||||
|
console.log(confirm_to_leave);
|
||||||
|
window.location.href = '/overview';
|
||||||
|
} else {
|
||||||
|
alert('Error');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label class="pl-2 w-2/6 flex gap-2 flex-row">
|
<svelte:window on:beforeunload={confirmUnload} />
|
||||||
<input type="checkbox" class="w-fit" bind:checked={data.public} />
|
{#await getEditID()}
|
||||||
<span class="w-fit">{$t('words.public')}?</span>
|
<Spinner />
|
||||||
</label>
|
{:then _}
|
||||||
<label class="pl-2 flex flex-row gap-2 w-3/5">
|
<form on:submit|preventDefault={saveQuiz}>
|
||||||
<span class="w-fit">{$t('words.title')}:</span>
|
<div class="grid grid-cols-6 h-screen w-screen">
|
||||||
<input
|
<div>
|
||||||
type="text"
|
<Sidebar bind:data bind:selected_question />
|
||||||
placeholder={$t('words.title')}
|
|
||||||
bind:value={data.title}
|
|
||||||
class="text-black bg-inherit border-dotted border-b-2 border-black w-full dark:text-gray-200"
|
|
||||||
class:border-red-600={!yup.reach(dataSchema, 'title').isValidSync(data.title)}
|
|
||||||
class:border-solid={!yup.reach(dataSchema, 'title').isValidSync(data.title)}
|
|
||||||
class:border-2={!yup.reach(dataSchema, 'title').isValidSync(data.title)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="pl-2 flex flex-row gap-2 w-3/5">
|
|
||||||
{$t('words.description')}:
|
|
||||||
<textarea
|
|
||||||
placeholder={$t('words.description')}
|
|
||||||
bind:value={data.description}
|
|
||||||
class="text-black w-full dark:text-gray-200 dark:bg-gray-800 rounded-lg p-1"
|
|
||||||
class:border-red-600={!yup.reach(dataSchema, 'description').isValidSync(data.description)}
|
|
||||||
class:border-solid={!yup.reach(dataSchema, 'description').isValidSync(data.description)}
|
|
||||||
class:border-2={!yup.reach(dataSchema, 'description').isValidSync(data.description)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{#each data.questions as question, index_question}
|
|
||||||
<div class="ml-8 grid grid-cols-1 gap-2 m-2 border border-black border-2">
|
|
||||||
<h1 class="text-3xl m-1">{$t('words.question')} {index_question + 1}</h1>
|
|
||||||
|
|
||||||
<label class="m-1 flex flex-row gap-2 w-3/5">
|
|
||||||
{$t('words.question')}:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={$t('words.question')}
|
|
||||||
bind:value={question.question}
|
|
||||||
class="text-black w-full bg-inherit border-dotted border-b-2 border-black dark:text-gray-200"
|
|
||||||
class:border-red-600={!yup
|
|
||||||
.reach(dataSchema, 'questions[].question')
|
|
||||||
.isValidSync(question.question)}
|
|
||||||
class:border-solid={!yup
|
|
||||||
.reach(dataSchema, 'questions[].question')
|
|
||||||
.isValidSync(question.question)}
|
|
||||||
class:border-2={!yup
|
|
||||||
.reach(dataSchema, 'questions[].question')
|
|
||||||
.isValidSync(question.question)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="m-1 flex flex-row gap-2 w-3/5">
|
|
||||||
{$t('words.image')}:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="https://i.imgur.com/kSPCidY.png"
|
|
||||||
bind:value={question.image}
|
|
||||||
class="text-black w-full bg-inherit border-dotted border-b-2 border-black dark:text-gray-200"
|
|
||||||
class:border-red-600={!yup
|
|
||||||
.reach(dataSchema, 'questions[].image')
|
|
||||||
.isValidSync(question.image)}
|
|
||||||
class:border-solid={!yup
|
|
||||||
.reach(dataSchema, 'questions[].image')
|
|
||||||
.isValidSync(question.image)}
|
|
||||||
class:border-2={!yup
|
|
||||||
.reach(dataSchema, 'questions[].image')
|
|
||||||
.isValidSync(question.image)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="m-1 flex flex-row gap-2 w-3/5 flex-nowrap whitespace-nowrap">
|
|
||||||
{$t('editor.time_in_seconds')}:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="20"
|
|
||||||
bind:value={question.time}
|
|
||||||
class="text-black w-full bg-inherit border-dotted border-b-2 border-black dark:text-gray-200"
|
|
||||||
class:border-red-600={!yup
|
|
||||||
.reach(dataSchema, 'questions[].time')
|
|
||||||
.isValidSync(question.time)}
|
|
||||||
class:border-solid={!yup
|
|
||||||
.reach(dataSchema, 'questions[].time')
|
|
||||||
.isValidSync(question.time)}
|
|
||||||
class:border-2={!yup
|
|
||||||
.reach(dataSchema, 'questions[].time')
|
|
||||||
.isValidSync(question.time)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{#each question.answers as answer, index_answer}
|
|
||||||
<div class="ml-8 grid grid-cols-1 gap-2 m-2 border border-black border-2 m-1">
|
|
||||||
<h1 class="text-3xl m-1">{$t('words.answer')} {index_answer + 1}</h1>
|
|
||||||
<p class="m-1">
|
|
||||||
{$t('words.answer')}: {index_answer + 1}
|
|
||||||
{$t('words.question')}: {index_question + 1}
|
|
||||||
</p>
|
|
||||||
<label class="m-1 flex flex-row gap-2 w-3/5 flex-nowrap whitespace-nowrap">
|
|
||||||
{$t('words.answer')}:
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={$t('words.answer')}
|
|
||||||
bind:value={data.questions[index_question].answers[index_answer].answer}
|
|
||||||
class="text-black w-full bg-inherit border-dotted border-b-2 border-black dark:text-gray-200"
|
|
||||||
class:border-red-600={!yup
|
|
||||||
.reach(dataSchema, 'questions[].answers[].answer')
|
|
||||||
.isValidSync(
|
|
||||||
data.questions[index_question].answers[index_answer].answer
|
|
||||||
)}
|
|
||||||
class:border-solid={!yup
|
|
||||||
.reach(dataSchema, 'questions[].answers[].answer')
|
|
||||||
.isValidSync(
|
|
||||||
data.questions[index_question].answers[index_answer].answer
|
|
||||||
)}
|
|
||||||
class:border-2={!yup
|
|
||||||
.reach(dataSchema, 'questions[].answers[].answer')
|
|
||||||
.isValidSync(
|
|
||||||
data.questions[index_question].answers[index_answer].answer
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="m-1 flex flex-row gap-2 w-2/6 flex-nowrap whitespace-nowrap">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={answer.right}
|
|
||||||
class="text-black w-fit dark:text-gray-200"
|
|
||||||
/>
|
|
||||||
<span class="w-fit">{$t('editor.right_or_true?')}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="text-left border-yellow-500 border-2 w-fit m-1"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
data.questions[index_question].answers.splice(index_answer, 1);
|
|
||||||
data.questions[index_question].answers =
|
|
||||||
data.questions[index_question].answers;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$t('editor.delete_answer')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<div class="col-span-5 flex flex-col">
|
||||||
<button
|
<div
|
||||||
class="text-left border-green-500 border-2 w-fit m-1"
|
class="h-10 w-full bg-white mb-10 flex align-middle justify-center rounded-br-lg"
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
data.questions[index_question].answers = [
|
|
||||||
...data.questions[index_question].answers,
|
|
||||||
{ ...empty_answer }
|
|
||||||
];
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{$t('editor.add_new_answer')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="text-left border-red-500 border-2 w-fit m-1"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
data.questions.splice(index_question, 1);
|
|
||||||
data.questions = data.questions;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$t('editor.delete_question')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<button
|
|
||||||
class="text-left"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
data.questions = [...data.questions, { ...empty_question }];
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$t('editor.add_new_question')}
|
|
||||||
</button>
|
|
||||||
{#if imgur_links_valid}
|
|
||||||
<p class="text-blue-600 text-xl text-center w-fit mx-auto">
|
|
||||||
{$t('editor.not_all_links_imgur_links')}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{#if schemaInvalid}
|
{#if schemaInvalid}
|
||||||
<p class="text-blue-600 text-xl text-center w-fit mx-auto">
|
<p class="text-center w-full text-red-600 h-full mt-0.5 font-semibold">
|
||||||
{yupErrorMessage}
|
{yupErrorMessage}
|
||||||
</p>
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center w-full text-black h-full align-bottom mt-0.5">
|
||||||
|
{data.title}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
class="pr-2 align-middle bg-purple-400 pl-2 ml-auto whitespace-nowrap disabled:opacity-60 rounded-br-lg"
|
||||||
class="text-xl disabled:cursor-not-allowed disabled:border disabled:border-red-500 w-fit mx-auto"
|
disabled={schemaInvalid}
|
||||||
disabled={imgur_links_valid || schemaInvalid}>{submit_button_text}</button
|
|
||||||
>
|
>
|
||||||
|
<span>Save</span>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
{#if selected_question === -1}
|
||||||
|
<SettingsCard bind:data />
|
||||||
|
{:else}
|
||||||
|
<QuizCard bind:data bind:selected_question bind:edit_id />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/await}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EditorData, Answer } from '$lib/quiz_types';
|
||||||
|
import CheckerBg from '$lib/editor/checker-bg.svg';
|
||||||
|
import { reach } from 'yup';
|
||||||
|
import { dataSchema } from '$lib/yupSchemas';
|
||||||
|
import Spinner from '../Spinner.svelte';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let data: EditorData;
|
||||||
|
export let selected_question: number;
|
||||||
|
export let edit_id: string;
|
||||||
|
|
||||||
|
let question = data.questions[selected_question];
|
||||||
|
$: question = data.questions[selected_question];
|
||||||
|
const empty_answer: Answer = {
|
||||||
|
right: false,
|
||||||
|
answer: ''
|
||||||
|
};
|
||||||
|
console.log(question.image);
|
||||||
|
let uppyOpen = false;
|
||||||
|
$: console.log(uppyOpen);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full pb-20 px-20">
|
||||||
|
<div class="rounded-lg bg-white w-full h-full border-gray-500 drop-shadow-2xl dark:bg-gray-700">
|
||||||
|
<div class="h-fit bg-gray-300 rounded-t-lg dark:bg-gray-500">
|
||||||
|
<div class="flex align-middle p-4 gap-3">
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-red-400 transition"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-yellow-400 transition"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-green-400 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex justify-center pt-10 w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={question.question}
|
||||||
|
placeholder="No title..."
|
||||||
|
class="p-3 rounded-lg border-gray-500 border text-center w-2/3 text-lg font-semibold placeholder:italic placeholder:font-normal dark:bg-gray-500"
|
||||||
|
class:bg-yellow-500={!reach(dataSchema, 'questions[].question').isValidSync(
|
||||||
|
question.question
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if question.image != undefined && question.image !== ''}
|
||||||
|
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
|
||||||
|
<img
|
||||||
|
src={question.image}
|
||||||
|
alt="not available"
|
||||||
|
class="max-h-72 h-auto w-auto"
|
||||||
|
on:contextmenu|preventDefault={() => {
|
||||||
|
question.image = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#await import('$lib/editor/uploader.svelte')}
|
||||||
|
<Spinner />
|
||||||
|
{:then c}
|
||||||
|
<svelte:component
|
||||||
|
this={c.default}
|
||||||
|
bind:modalOpen={uppyOpen}
|
||||||
|
bind:edit_id
|
||||||
|
bind:data
|
||||||
|
bind:selected_question
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
<div class="flex justify-center pt-10 w-full">
|
||||||
|
<div class="grid grid-cols-2 gap-4 w-full px-10">
|
||||||
|
{#each question.answers as answer, index}
|
||||||
|
<div
|
||||||
|
on:contextmenu|preventDefault={() => {
|
||||||
|
question.answers.splice(index, 1);
|
||||||
|
question.answers = question.answers;
|
||||||
|
}}
|
||||||
|
out:fade={{ duration: 150 }}
|
||||||
|
class="p-4 rounded-lg flex justify-center w-full transition"
|
||||||
|
class:bg-red-500={!answer.right}
|
||||||
|
class:bg-green-500={answer.right}
|
||||||
|
class:bg-yellow-500={!reach(
|
||||||
|
dataSchema,
|
||||||
|
'questions[].answers[].answer'
|
||||||
|
).isValidSync(answer.answer)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
bind:value={answer.answer}
|
||||||
|
type="text"
|
||||||
|
class="bg-transparent border-b-2 border-dotted w-5/6 text-center"
|
||||||
|
placeholder="Empty..."
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
answer.right = !answer.right;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if answer.right}
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if question.answers.length < 4}
|
||||||
|
<button
|
||||||
|
class="p-4 rounded-lg bg-transparent border-gray-500 border-2 hover:bg-gray-300 transition dark:hover:bg-gray-600"
|
||||||
|
type="button"
|
||||||
|
in:fade={{ duration: 150 }}
|
||||||
|
on:click={() => {
|
||||||
|
question.answers = [...question.answers, { empty_answer }];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="italic text-center">Add an answer</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="italic text-center mt-auto pt-4">Right-click on an answer to delete it!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="16" height="16">
|
||||||
|
<path fill="#d0d0d0" fill-rule="evenodd" d="M0 0h4v4H0V0zm4 4h4v4H4V4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 167 B |
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EditorData } from '$lib/quiz_types';
|
||||||
|
|
||||||
|
export let data: EditorData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full h-full pb-20 px-20">
|
||||||
|
<div class="rounded-lg bg-white w-full h-full border-gray-500 dark:bg-gray-700">
|
||||||
|
<div class="h-fit bg-gray-300 rounded-t-lg dark:bg-gray-500">
|
||||||
|
<div class="flex align-middle p-4 gap-3">
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-red-400 transition"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-yellow-400 transition"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="inline-block bg-gray-600 w-4 h-4 rounded-full hover:bg-green-400 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dark:bg-gray-700">
|
||||||
|
<div class="flex justify-center pt-10 w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={data.title}
|
||||||
|
class="p-3 rounded-lg border-gray-500 border text-center w-1/3 text-lg font-semibold dark:bg-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center pt-10 w-full max-h-32">
|
||||||
|
<textarea
|
||||||
|
type="text"
|
||||||
|
bind:value={data.description}
|
||||||
|
class="p-3 rounded-lg border-gray-500 border text-center w-1/3 h-20 resize-none dark:bg-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pt-10 w-full flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
data.public = !data.public;
|
||||||
|
}}
|
||||||
|
class="text-center w-fit"
|
||||||
|
>
|
||||||
|
{#if data.public}
|
||||||
|
<svg
|
||||||
|
class="w-8 h-8 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Public</span>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
class="w-8 h-8 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Private</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EditorData, Question } from '../quiz_types';
|
||||||
|
import { reach } from 'yup';
|
||||||
|
import { dataSchema } from '../yupSchemas';
|
||||||
|
|
||||||
|
export let data: EditorData;
|
||||||
|
export let selected_question = -1;
|
||||||
|
|
||||||
|
import { createTippy } from 'svelte-tippy';
|
||||||
|
import 'tippy.js/animations/perspective-subtle.css';
|
||||||
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
|
||||||
|
const tippy = createTippy({
|
||||||
|
arrow: true,
|
||||||
|
animation: 'perspective-subtle',
|
||||||
|
placement: 'right'
|
||||||
|
});
|
||||||
|
let arr_of_cards = Array(data.questions.length);
|
||||||
|
let propertyCard;
|
||||||
|
const empty_question: Question = {
|
||||||
|
question: '',
|
||||||
|
time: '20',
|
||||||
|
image: '',
|
||||||
|
answers: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSelectedQuestion = (index: number): void => {
|
||||||
|
selected_question = index;
|
||||||
|
if (index === -1) {
|
||||||
|
propertyCard.scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
arr_of_cards[index].scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/* onMount(() => {
|
||||||
|
propertyCard.scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
});*/
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-screen border-r-2 pt-6 px-6 overflow-scroll">
|
||||||
|
<div
|
||||||
|
class="bg-white shadow rounded-lg h-40 p-2 mb-6 hover:cursor-pointer drop-shadow-2xl border border-gray-500 dark:bg-gray-600"
|
||||||
|
bind:this={propertyCard}
|
||||||
|
class:bg-green-300={selected_question === -1}
|
||||||
|
class:dark:bg-green-500={selected_question === -1}
|
||||||
|
on:click={() => setSelectedQuestion(-1)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
use:tippy={{ content: data.title === '' ? "It's empty!" : data.title }}
|
||||||
|
class="m-1 border border-gray-500 rounded-lg p-0.5"
|
||||||
|
class:border-red-600={!reach(dataSchema, 'title').isValidSync(data.title)}
|
||||||
|
class:border-solid={!reach(dataSchema, 'title').isValidSync(data.title)}
|
||||||
|
class:border-2={!reach(dataSchema, 'title').isValidSync(data.title)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="whitespace-nowrap truncate text-center w-full bg-transparent rounded font-semibold dark:text-white"
|
||||||
|
class:dark:text-black={selected_question === -1}
|
||||||
|
bind:value={data.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
use:tippy={{ content: data.description === '' ? "It's empty!" : data.description }}
|
||||||
|
class="m-1 border border-gray-500 rounded-lg p-0.5"
|
||||||
|
class:border-red-600={!reach(dataSchema, 'description').isValidSync(data.description)}
|
||||||
|
class:border-solid={!reach(dataSchema, 'description').isValidSync(data.description)}
|
||||||
|
class:border-2={!reach(dataSchema, 'description').isValidSync(data.description)}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
bind:value={data.description}
|
||||||
|
class="bg-transparent resize-none w-full rounded text-sm dark:text-white"
|
||||||
|
class:dark:text-black={selected_question === -1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full flex justify-center dark:text-white"
|
||||||
|
class:dark:text-black={selected_question === -1}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
data.public = !data.public;
|
||||||
|
}}
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{#if data.public}
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Public</span>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Private</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#each data.questions as question, index}
|
||||||
|
<div
|
||||||
|
class="bg-white shadow rounded-lg h-40 p-2 mb-6 hover:cursor-pointer drop-shadow-2xl border border-gray-500 dark:bg-gray-600"
|
||||||
|
class:bg-green-300={index === selected_question}
|
||||||
|
class:dark:bg-green-500={index === selected_question}
|
||||||
|
on:contextmenu|preventDefault={() => {
|
||||||
|
if (confirm('Do you really want to delete this Question?')) {
|
||||||
|
data.questions.splice(index, 1);
|
||||||
|
data.questions = data.questions;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:click={() => {
|
||||||
|
setSelectedQuestion(index);
|
||||||
|
}}
|
||||||
|
bind:this={arr_of_cards[index]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
use:tippy={{ content: question.question === '' ? 'No title' : question.question }}
|
||||||
|
class="m-1 border border-gray-500 rounded-lg p-0.5"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="whitespace-nowrap truncate text-center rounded-lg dark:text-white transition"
|
||||||
|
class:bg-yellow-500={!reach(dataSchema, 'questions[].question').isValidSync(
|
||||||
|
question.question
|
||||||
|
)}
|
||||||
|
class:dark:text-black={index === selected_question}
|
||||||
|
>
|
||||||
|
{#if question.question === ''}
|
||||||
|
<span class="italic text-gray-500">No title...</span>
|
||||||
|
{:else}
|
||||||
|
{question.question}
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{#if question.image}
|
||||||
|
<div class="flex justify-center align-middle pb-0.5">
|
||||||
|
<img
|
||||||
|
src={question.image}
|
||||||
|
class="h-10 border rounded-lg"
|
||||||
|
alt="Not available"
|
||||||
|
use:tippy={{
|
||||||
|
content: `<img src='${question.image}' alt='Not available' class='rounded'>`,
|
||||||
|
allowHTML: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{#each question.answers as answer}
|
||||||
|
<span
|
||||||
|
class="whitespace-nowrap truncate rounded-lg p-0.5 text-sm text-center"
|
||||||
|
class:bg-green-500={answer.right}
|
||||||
|
class:bg-red-500={!answer.right}
|
||||||
|
class:bg-yellow-500={!reach(
|
||||||
|
dataSchema,
|
||||||
|
'questions[].answers[].answer'
|
||||||
|
).isValidSync(answer.answer)}
|
||||||
|
use:tippy={{ content: answer.answer === '' ? 'Empty...' : answer.answer }}
|
||||||
|
>{#if answer.answer === ''}
|
||||||
|
<i>Empty...</i>
|
||||||
|
{:else}
|
||||||
|
{answer.answer}
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div
|
||||||
|
class="bg-white shadow rounded-lg h-40 p-2 mb-6 hover:cursor-pointer drop-shadow-2xl border border-gray-500"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-full flex justify-center w-full dark:text-black"
|
||||||
|
on:click={() => {
|
||||||
|
data.questions = [...data.questions, { ...empty_question }];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-full"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dashboard as SvelteDashboard } from '@uppy/svelte';
|
||||||
|
import Uppy from '@uppy/core';
|
||||||
|
import DropTarget from '@uppy/drop-target';
|
||||||
|
import XHRUpload from '@uppy/xhr-upload';
|
||||||
|
import ImageEditor from '@uppy/image-editor';
|
||||||
|
import Dashboard from '@uppy/dashboard';
|
||||||
|
import Compressor from '@uppy/compressor';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
// CSS imports
|
||||||
|
import '@uppy/core/dist/style.css';
|
||||||
|
import '@uppy/dashboard/dist/style.css';
|
||||||
|
import '@uppy/drop-target/dist/style.css';
|
||||||
|
// import '@uppy/file-input/dist/style.css'
|
||||||
|
import '@uppy/image-editor/dist/style.css';
|
||||||
|
import type { EditorData } from '../quiz_types';
|
||||||
|
|
||||||
|
export let modalOpen = false;
|
||||||
|
export let edit_id: string;
|
||||||
|
export let data: EditorData;
|
||||||
|
export let selected_question: number;
|
||||||
|
|
||||||
|
const uppy = new Uppy()
|
||||||
|
.use(DropTarget, {
|
||||||
|
target: document.body
|
||||||
|
})
|
||||||
|
.use(Dashboard)
|
||||||
|
.use(ImageEditor, {
|
||||||
|
target: Dashboard,
|
||||||
|
quality: 0.8
|
||||||
|
})
|
||||||
|
.use(Compressor, {
|
||||||
|
quality: 0.6
|
||||||
|
})
|
||||||
|
.use(XHRUpload, {
|
||||||
|
endpoint: `/api/v1/editor/image?edit_id=${edit_id}`
|
||||||
|
});
|
||||||
|
const props = { inline: true };
|
||||||
|
let image_id;
|
||||||
|
uppy.on('upload-success', (file, response) => {
|
||||||
|
image_id = response.body.id;
|
||||||
|
});
|
||||||
|
uppy.on('complete', (res) => {
|
||||||
|
data.questions[
|
||||||
|
selected_question
|
||||||
|
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
|
||||||
|
modalOpen = false;
|
||||||
|
});
|
||||||
|
console.log(edit_id);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if modalOpen}
|
||||||
|
<div
|
||||||
|
class="w-full h-full absolute top-0 left-0 bg-opacity-60 z-20 flex justify-center"
|
||||||
|
transition:fade
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-t-lg bg-black text-white px-1"
|
||||||
|
on:click={() => {
|
||||||
|
modalOpen = false;
|
||||||
|
}}
|
||||||
|
>Close
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<SvelteDashboard {uppy} width="100%" {props} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex justify-center w-full pt-10" transition:fade>
|
||||||
|
<button
|
||||||
|
class="rounded-lg p-4 flex justify-center bg-transparent border-gray-500 border-2 w-1/2 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
modalOpen = true;
|
||||||
|
}}
|
||||||
|
><span class="italic">Add Image</span>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 inline-block"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -23,7 +23,7 @@ export const initLocalizationContext = () => {
|
|||||||
const i18n = new I18nService();
|
const i18n = new I18nService();
|
||||||
const tranlator = new I18NextTranslationService(i18n);
|
const tranlator = new I18NextTranslationService(i18n);
|
||||||
|
|
||||||
// Setting the Svelte context
|
// skipcq: JS-0357
|
||||||
setLocalization({
|
setLocalization({
|
||||||
t: tranlator.translate,
|
t: tranlator.translate,
|
||||||
currentLanguage: tranlator.locale
|
currentLanguage: tranlator.locale
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/* import AdminOverviewScreenshot from '$lib/assets/landing/admin_overview.png';
|
||||||
|
import SelectScreenshot from '$lib/assets/landing/select.webp';
|
||||||
|
import ResultsScreenshot from '$lib/assets/landing/results.webp';
|
||||||
|
import SolutionScreenshot from '$lib/assets/landing/solution.webp';*/
|
||||||
|
import SelectMockup from '$lib/assets/landing/select-mockup.webp';
|
||||||
|
import ResultsMockup from '$lib/assets/landing/results-mockup.webp';
|
||||||
|
import SolutionMockup from '$lib/assets/landing/solution-mockup.webp';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 grow pt-4">
|
||||||
|
<h1 class="text-center h-fit text-3xl marck-script">1: Answer</h1>
|
||||||
|
<h1 class="text-center h-fit text-3xl marck-script">2: See what was right or wrong</h1>
|
||||||
|
<h1 class="text-center h-fit text-3xl marck-script">3: See how many were right or wrong</h1>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img src={SelectMockup} class="max-h-80" alt="Select answer" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img src={SolutionMockup} class="max-h-80" alt="See right and wrong solutions" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img
|
||||||
|
src={ResultsMockup}
|
||||||
|
class="max-h-80"
|
||||||
|
alt="See results in comparison with other players"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -20,6 +20,13 @@ export interface Answer {
|
|||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditorData {
|
||||||
|
public: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
questions: Question[];
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Keep an eye on this shit
|
// TODO Keep an eye on this shit
|
||||||
// export interface Answer {
|
// export interface Answer {
|
||||||
// username: string;
|
// username: string;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ export const dataSchema = yup.object({
|
|||||||
.max(300, 'The title has to be shorter than 300 characters'),
|
.max(300, 'The title has to be shorter than 300 characters'),
|
||||||
description: yup
|
description: yup
|
||||||
.string()
|
.string()
|
||||||
.required()
|
.required('The description is required!')
|
||||||
.min(3, 'The title has to be longer than 3 characters')
|
.min(3, 'The description has to be longer than 3 characters')
|
||||||
.max(300, 'The title has to be shorter than 300 characters'),
|
.max(500, 'The description has to be shorter than 500 characters'),
|
||||||
questions: yup
|
questions: yup
|
||||||
.array()
|
.array()
|
||||||
.of(
|
.of(
|
||||||
yup.object({
|
yup.object({
|
||||||
question: yup.string().required('A question is required').max(299),
|
question: yup.string().required('A question-title is required').max(299),
|
||||||
time: yup.number().required().positive('The time has to be positive'),
|
time: yup.number().required().positive('The time has to be positive'),
|
||||||
image: yup
|
image: yup
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
// height: 100%;
|
// height: 100%;
|
||||||
// width: 100%;
|
// width: 100%;
|
||||||
|
|
||||||
|
// bg-gradient-to-r from-[#009444] via-[#39b54a] to-[#8dc63f]
|
||||||
background: linear-gradient(to right, #009444, #39b54a, #8dc63f) repeat-y;
|
background: linear-gradient(to right, #009444, #39b54a, #8dc63f) repeat-y;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/*background: linear-gradient(-225deg, #231557 0%, #44107A 29%, #FF1361 67%, #FFF800 100%); */
|
/*background: linear-gradient(-225deg, #231557 0%, #44107A 29%, #FF1361 67%, #FFF800 100%); */
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
if (session.authenticated) {
|
if (session.authenticated) {
|
||||||
signedIn.set(true);
|
signedIn.set(true);
|
||||||
}
|
}
|
||||||
return {}
|
return {};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
import { navbarVisible } from '$lib/stores';
|
import { navbarVisible } from '$lib/stores';
|
||||||
import { dataSchema } from '$lib/yupSchemas';
|
import { dataSchema } from '$lib/yupSchemas';
|
||||||
|
|
||||||
navbarVisible.set(true);
|
navbarVisible.set(false);
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
let data: Data;
|
let data: Data;
|
||||||
let confirm_to_leave = true;
|
let confirm_to_leave = true;
|
||||||
|
let quiz_id = null;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const from_localstorage = localStorage.getItem('create_game');
|
const from_localstorage = localStorage.getItem('create_game');
|
||||||
if (from_localstorage === null) {
|
if (from_localstorage === null) {
|
||||||
@@ -63,45 +64,14 @@
|
|||||||
data = JSON.parse(from_localstorage);
|
data = JSON.parse(from_localstorage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!(await dataSchema.isValid(data))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await fetch('/api/v1/quiz/create', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (res.status === 401) {
|
|
||||||
localStorage.setItem('create_game', JSON.stringify(data));
|
|
||||||
window.location.href = '/account/login';
|
|
||||||
} else if (res.status === 200) {
|
|
||||||
localStorage.removeItem('create_game');
|
|
||||||
responseData.open = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const confirmUnload = () => {
|
|
||||||
if (!confirm_to_leave) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.returnValue = '';
|
|
||||||
localStorage.setItem('create_game', JSON.stringify(data));
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={confirmUnload} />
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>ClassQuiz - Create</title>
|
<title>ClassQuiz - Create</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if data !== undefined}
|
{#if data !== undefined}
|
||||||
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
|
<Editor bind:data bind:quiz_id />
|
||||||
<Editor bind:data />
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
import { signedIn } from '$lib/stores';
|
import { signedIn } from '$lib/stores';
|
||||||
|
|
||||||
export async function load({ url, session }) {
|
export async function load({ url, session }) {
|
||||||
|
const quiz_id = url.searchParams.get('quiz_id');
|
||||||
if (!session.authenticated) {
|
if (!session.authenticated) {
|
||||||
return {
|
return {
|
||||||
status: 302,
|
status: 302,
|
||||||
redirect: '/account/login?returnTo=/edit'
|
redirect: `/account/login?returnTo=/edit?quiz_id=${quiz_id}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.authenticated) {
|
if (session.authenticated) {
|
||||||
signedIn.set(true);
|
signedIn.set(true);
|
||||||
}
|
}
|
||||||
const quiz_id = url.searchParams.get('quiz_id');
|
|
||||||
if (quiz_id === null) {
|
if (quiz_id === null) {
|
||||||
return {
|
return {
|
||||||
status: 404
|
status: 404
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
import { navbarVisible } from '$lib/stores';
|
import { navbarVisible } from '$lib/stores';
|
||||||
import { dataSchema } from '$lib/yupSchemas';
|
import { dataSchema } from '$lib/yupSchemas';
|
||||||
|
|
||||||
navbarVisible.set(true);
|
navbarVisible.set(false);
|
||||||
|
|
||||||
const { t } = getLocalization();
|
const { t } = getLocalization();
|
||||||
|
|
||||||
@@ -72,38 +73,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const submit = async () => {
|
|
||||||
if (!(await dataSchema.isValid(data))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await fetch(`/api/v1/quiz/update/${quiz_id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (res.status === 401) {
|
|
||||||
throw new Error('Unauthorized');
|
|
||||||
} else if (res.status === 404) {
|
|
||||||
throw new Error('Quiz not found');
|
|
||||||
} else if (res.status === 200) {
|
|
||||||
localStorage.removeItem('edit_game');
|
|
||||||
responseData.data = '200';
|
|
||||||
responseData.open = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const confirmUnload = () => {
|
|
||||||
if (!confirm_to_leave) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.returnValue = '';
|
|
||||||
localStorage.setItem('edit_game', JSON.stringify(data));
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:beforeunload={confirmUnload} />
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>ClassQuiz - Edit</title>
|
<title>ClassQuiz - Edit</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
@@ -120,9 +91,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{:then _}
|
{:then _}
|
||||||
{#if data !== undefined}
|
{#if data !== undefined}
|
||||||
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
|
<Editor bind:data submit_button_text={$t('words.save')} bind:quiz_id />
|
||||||
<Editor bind:data submit_button_text={$t('words.save')} />
|
|
||||||
</form>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:catch err}
|
{:catch err}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user