From 6d5fac39fd0e48566f4992ed9d76437b419bf2c1 Mon Sep 17 00:00:00 2001 From: Mawoka Date: Fri, 17 Jun 2022 00:11:08 +0200 Subject: [PATCH] :construction: Last change --- classquiz/__init__.py | 23 +- classquiz/config.py | 5 +- classquiz/routers/editor.py | 104 +++++ frontend/package.json | 7 + frontend/pnpm-lock.yaml | 375 ++++++++++++++++++- frontend/src/lib/clickOutside.js | 16 + frontend/src/lib/editor.svelte | 109 ++++-- frontend/src/lib/editor/card.svelte | 36 +- frontend/src/lib/editor/settings-card.svelte | 10 +- frontend/src/lib/editor/sidebar.svelte | 20 +- frontend/src/lib/editor/uploader.svelte | 72 ++++ frontend/src/routes/create.svelte | 3 +- frontend/src/routes/edit.svelte | 2 +- 13 files changed, 705 insertions(+), 77 deletions(-) create mode 100644 classquiz/routers/editor.py create mode 100644 frontend/src/lib/clickOutside.js create mode 100644 frontend/src/lib/editor/uploader.svelte diff --git a/classquiz/__init__.py b/classquiz/__init__.py index 598ee33..3a95a85 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -6,15 +6,15 @@ from starlette.middleware.sessions import SessionMiddleware from classquiz.config import settings 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.helpers import meilisearch_init settings = settings() if settings.sentry_dsn: sentry_sdk.init(dsn=settings.sentry_dsn, integrations=[RedisIntegration()]) -# app = FastAPI(redoc_url="", docs_url="/api/docs") -app = FastAPI(redoc_url="", docs_url="") +app = FastAPI(redoc_url="", docs_url="/api/docs") +# app = FastAPI(redoc_url="", docs_url="") app.state.database = database @@ -46,11 +46,14 @@ async def shutdown() -> None: app.add_middleware(SessionMiddleware, secret_key=settings.secret_key) -app.include_router(users.router, tags=["users"], prefix="/api/v1/users") -app.include_router(quiz.router, tags=["quiz"], prefix="/api/v1/quiz") -app.include_router(utils.router, tags=["utils"], prefix="/api/v1/utils") -app.include_router(stats.router, tags=["stats"], prefix="/api/v1/stats") -app.include_router(storage.router, tags=["storage"], prefix="/api/v1/storage") -app.include_router(search.router, tags=["search"], prefix="/api/v1/search") -app.include_router(testing_routes.router, tags=["internal", "testing"], prefix="/api/v1/internal/testing") +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", include_in_schema=False) +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", include_in_schema=True) +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", include_in_schema=True) +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)) diff --git a/classquiz/config.py b/classquiz/config.py index 2677d74..b26db82 100644 --- a/classquiz/config.py +++ b/classquiz/config.py @@ -1,6 +1,7 @@ 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 import meilisearch as MeiliSearch from typing import Optional @@ -53,7 +54,7 @@ def settings() -> 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( backend=settings().storage_backend, deta_key=settings().deta_project_key, diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py new file mode 100644 index 0000000..5866362 --- /dev/null +++ b/classquiz/routers/editor.py @@ -0,0 +1,104 @@ +import asyncio +import html +import uuid +from typing import Optional + +import bleach +import pydantic +from fastapi import APIRouter, File, Form, UploadFile, HTTPException, BackgroundTasks, 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 + +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: + if 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)) + if session_data.edit: + quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id) + 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"] + return await quiz.save() + 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)]) + return await quiz.save() diff --git a/frontend/package.json b/frontend/package.json index 8e0daa5..39b12f3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,13 @@ "@types/yup": "^0.29.14", "@typescript-eslint/eslint-plugin": "^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", "cookie": "^0.5.0", "cssnano": "^5.1.11", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index be0c333..beda530 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -18,6 +18,13 @@ specifiers: '@types/yup': ^0.29.14 '@typescript-eslint/eslint-plugin': ^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 cookie: ^0.5.0 cssnano: ^5.1.11 @@ -73,6 +80,13 @@ devDependencies: '@types/yup': 0.29.14 '@typescript-eslint/eslint-plugin': 5.27.1_aq7uryhocdbvbqum33pitcm3y4 '@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 cookie: 0.5.0 cssnano: 5.1.11_postcss@8.4.14 @@ -592,6 +606,20 @@ packages: tailwindcss: 3.1.2 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: resolution: { @@ -822,6 +850,226 @@ packages: eslint-visitor-keys: 3.3.0 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: resolution: { @@ -1061,6 +1309,13 @@ packages: file-uri-to-path: 1.0.0 dev: true + /blueimp-canvas-to-blob/3.29.0: + resolution: + { + integrity: sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg== + } + dev: true + /boolbase/1.0.0: resolution: { @@ -1189,6 +1444,13 @@ packages: engines: { node: '>=10' } dev: true + /classnames/2.3.1: + resolution: + { + integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + } + dev: true + /cluster-key-slot/1.1.0: resolution: { @@ -1245,6 +1507,16 @@ packages: engines: { node: '>= 10' } 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: resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= } dev: true @@ -1271,6 +1543,13 @@ packages: } dev: true + /cropperjs/1.5.7: + resolution: + { + integrity: sha512-sGj+G/ofKh+f6A4BtXLJwtcKJgMUsXYVUubfTo9grERiDGXncttefmue/fyQFvn8wfdyoD1KhDRYLfjkJFl0yw== + } + dev: true + /cross-spawn/7.0.3: resolution: { @@ -2170,6 +2449,13 @@ packages: engines: { node: '>=0.10.0' } dev: true + /exifr/7.1.3: + resolution: + { + integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw== + } + dev: true + /fast-deep-equal/3.1.3: resolution: { @@ -2672,6 +2958,14 @@ packages: binary-extensions: 2.2.0 dev: true + /is-blob/2.1.0: + resolution: + { + integrity: sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw== + } + engines: { node: '>=6' } + dev: true + /is-core-module/2.9.0: resolution: { @@ -2725,6 +3019,13 @@ packages: engines: { node: '>=0.12.0' } dev: true + /is-shallow-equal/1.0.1: + resolution: + { + integrity: sha512-lq5RvK+85Hs5J3p4oA4256M1FEffzmI533ikeDHvJd42nouRRx5wBzt36JuviiGe5dIPyHON/d0/Up+PBo6XkQ== + } + dev: true + /isarray/1.0.0: resolution: { @@ -2819,6 +3120,13 @@ packages: } dev: true + /lodash.debounce/4.0.8: + resolution: + { + integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + } + dev: true + /lodash.defaults/4.2.0: resolution: { @@ -2854,6 +3162,13 @@ packages: } dev: true + /lodash.throttle/4.1.1: + resolution: + { + integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + } + dev: true + /lodash.uniq/4.5.0: resolution: { @@ -2953,6 +3268,13 @@ packages: } dev: true + /memoize-one/5.2.1: + resolution: + { + integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + } + dev: true + /merge2/1.4.1: resolution: { @@ -2972,6 +3294,15 @@ packages: picomatch: 2.3.1 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: resolution: { @@ -3092,6 +3423,13 @@ packages: } dev: true + /namespace-emitter/2.0.1: + resolution: + { + integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g== + } + dev: true + /nanoclone/0.2.1: resolution: { @@ -3938,6 +4276,13 @@ packages: } dev: true + /preact/10.8.1: + resolution: + { + integrity: sha512-p5CKQ0MCEXTGKGOHiFaNE2V2nDq2hvDHykXvIlz+4lbfJ9umLZr8JS/fa1bXUwRcHXK+Ljk8zqmDhr25n0LtVg== + } + dev: true + /prelude-ls/1.2.1: resolution: { @@ -3975,6 +4320,14 @@ packages: } 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: resolution: { @@ -4979,11 +5332,17 @@ packages: dev: true /webidl-conversions/3.0.1: - resolution: { integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= } + resolution: + { + integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + } dev: true /whatwg-url/5.0.0: - resolution: { integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0= } + resolution: + { + integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + } dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 @@ -5009,6 +5368,13 @@ packages: string-width: 4.2.3 dev: true + /wildcard/1.1.2: + resolution: + { + integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng== + } + dev: true + /word-wrap/1.2.3: resolution: { @@ -5029,7 +5395,10 @@ packages: dev: true /wrappy/1.0.2: - resolution: { integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= } + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + } dev: true /ws/8.2.3: diff --git a/frontend/src/lib/clickOutside.js b/frontend/src/lib/clickOutside.js new file mode 100644 index 0000000..cfaa194 --- /dev/null +++ b/frontend/src/lib/clickOutside.js @@ -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); + } + }; +} diff --git a/frontend/src/lib/editor.svelte b/frontend/src/lib/editor.svelte index 86834a7..4129e5b 100644 --- a/frontend/src/lib/editor.svelte +++ b/frontend/src/lib/editor.svelte @@ -6,6 +6,7 @@ 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(); @@ -14,6 +15,7 @@ export let data: EditorData; export let submit_button_text = 'Create'; + export let quiz_id: string | null; let selected_question = -1; let imgur_links_valid = false; @@ -63,48 +65,75 @@ right: false, answer: '' }; + let edit_id; + + 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!'); + } + }; -
-
- -
-
-
- {#if schemaInvalid} -

- {yupErrorMessage} -

- {:else} -

{data.title}

- {/if} - -
-
- {#if selected_question === -1} - - {:else} - - {/if} + Save + + + + +
+
+ {#if selected_question === -1} + + {:else} + + {/if} +
- +{/await} diff --git a/frontend/src/lib/editor/card.svelte b/frontend/src/lib/editor/card.svelte index 011a2e9..6ff4421 100644 --- a/frontend/src/lib/editor/card.svelte +++ b/frontend/src/lib/editor/card.svelte @@ -3,9 +3,11 @@ import CheckerBg from '$lib/editor/checker-bg.svg'; import { reach } from 'yup'; import { dataSchema } from '$lib/yupSchemas'; + import Spinner from '../Spinner.svelte'; 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]; @@ -13,11 +15,14 @@ right: false, answer: '' }; + console.log(question.image); + let uppyOpen = false; + $: console.log(uppyOpen);
-
-
+
+
-
- not available -
+ {#if question.image != undefined && question.image !== ''} +
+ not available +
+ {:else} + {#await import('$lib/editor/uploader.svelte')} + + {:then c} + + {/await} + {/if}
{#each question.answers as answer, index} @@ -109,8 +127,7 @@ {/each} {#if question.answers.length < 4}
+{#if uppyOpen} + 1 +{/if} diff --git a/frontend/src/lib/editor/settings-card.svelte b/frontend/src/lib/editor/settings-card.svelte index 4f47e91..710456b 100644 --- a/frontend/src/lib/editor/settings-card.svelte +++ b/frontend/src/lib/editor/settings-card.svelte @@ -5,8 +5,8 @@
-
-
+
+
-
+