🚧 Last change

This commit is contained in:
Mawoka
2022-06-17 00:11:08 +02:00
parent d7ee89d640
commit 6d5fac39fd
13 changed files with 705 additions and 77 deletions
+13 -10
View File
@@ -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))
+3 -2
View File
@@ -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,
+104
View File
@@ -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()
+7
View File
@@ -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",
+372 -3
View File
@@ -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:
+16
View File
@@ -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);
}
};
}
+69 -40
View File
@@ -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!');
}
};
</script>
<div class="grid grid-cols-6 h-screen w-screen">
<div>
<Sidebar bind:data bind:selected_question />
</div>
<div class="col-span-5 flex flex-col">
<div class="h-10 w-full bg-white mb-10 flex align-middle justify-center rounded-br-lg">
{#if schemaInvalid}
<p class="text-center w-full text-red-600 h-full mt-0.5 font-semibold">
{yupErrorMessage}
</p>
{:else}
<p class="text-center w-full text-black h-full align-bottom mt-0.5">{data.title}</p>
{/if}
<button
class="pr-2 align-middle bg-purple-400 pl-2 ml-auto whitespace-nowrap disabled:opacity-60 rounded-br-lg"
disabled={schemaInvalid}
>
<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"
{#await getEditID()}
<Spinner />
{:then _}
<div class="grid grid-cols-6 h-screen w-screen">
<div>
<Sidebar bind:data bind:selected_question />
</div>
<div class="col-span-5 flex flex-col">
<div class="h-10 w-full bg-white mb-10 flex align-middle justify-center rounded-br-lg">
{#if schemaInvalid}
<p class="text-center w-full text-red-600 h-full mt-0.5 font-semibold">
{yupErrorMessage}
</p>
{:else}
<p class="text-center w-full text-black h-full align-bottom mt-0.5">
{data.title}
</p>
{/if}
<button
class="pr-2 align-middle bg-purple-400 pl-2 ml-auto whitespace-nowrap disabled:opacity-60 rounded-br-lg"
disabled={schemaInvalid}
>
<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 />
{/if}
<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>
</div>
{/await}
+28 -8
View File
@@ -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);
</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">
<div class="h-fit bg-gray-300 rounded-t-lg">
<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"
@@ -36,15 +41,28 @@
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"
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>
<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" />
</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" />
</div>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<Spinner />
{:then c}
<svelte:component
this={c.default}
bind:modalOpen={uppyOpen}
bind:edit_id
bind:data
/>
{/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}
@@ -109,8 +127,7 @@
{/each}
{#if question.answers.length < 4}
<button
class="p-4 rounded-lg bg-transparent"
style="background-image: url({CheckerBg})"
class="p-4 rounded-lg bg-transparent border-gray-500 border-2"
type="button"
on:click={() => {
question.answers = [...question.answers, { empty_answer }];
@@ -125,3 +142,6 @@
</div>
</div>
</div>
{#if uppyOpen}
<span class="fixed w-screen h-screen bg-opacity-60 z-10">1</span>
{/if}
+5 -5
View File
@@ -5,8 +5,8 @@
</script>
<div class="w-full h-full pb-20 px-20">
<div class="rounded-lg bg-white w-full h-full border-gray-500">
<div class="h-fit bg-gray-300 rounded-t-lg">
<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"
@@ -19,19 +19,19 @@
/>
</div>
</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"
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"
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">
+13 -7
View File
@@ -45,9 +45,10 @@
<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"
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
@@ -59,7 +60,7 @@
>
<input
type="text"
class="whitespace-nowrap truncate text-center w-full bg-transparent rounded font-semibold"
class="whitespace-nowrap truncate text-center w-full bg-transparent rounded font-semibold dark:text-black"
bind:value={data.title}
/>
</div>
@@ -72,10 +73,10 @@
>
<textarea
bind:value={data.description}
class="bg-transparent resize-none w-full rounded text-sm"
class="bg-transparent resize-none w-full rounded text-sm dark:text-black"
/>
</div>
<div class="w-full flex justify-center">
<div class="w-full flex justify-center dark:text-black">
<button
type="button"
on:click={() => {
@@ -121,8 +122,13 @@
</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"
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={() => {
data.questions.splice(index, 1);
data.questions = data.questions;
}}
on:click={() => {
setSelectedQuestion(index);
}}
@@ -133,7 +139,7 @@
class="m-1 border border-gray-500 rounded-lg p-0.5"
>
<h1
class="whitespace-nowrap truncate text-center rounded-lg"
class="whitespace-nowrap truncate text-center rounded-lg dark:text-black"
class:bg-yellow-500={!reach(dataSchema, 'questions[].question').isValidSync(
question.question
)}
@@ -184,7 +190,7 @@
>
<button
type="button"
class="h-full flex justify-center w-full"
class="h-full flex justify-center w-full dark:text-black"
on:click={() => {
data.questions = [...data.questions, { ...empty_question }];
}}
+72
View File
@@ -0,0 +1,72 @@
<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';
// 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 = `https://${window.location.hostname}/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">
<div>
<button
type="button"
on:click={() => {
modalOpen = false;
}}>Close</button
>
<div>
<SvelteDashboard {uppy} width="100%" {props} />
</div>
</div>
</div>
{:else}
<button
on:click={() => {
modalOpen = true;
}}>Add Image</button
>
{/if}
+2 -1
View File
@@ -50,6 +50,7 @@
let data: Data;
let confirm_to_leave = true;
let quiz_id = null;
onMount(() => {
const from_localstorage = localStorage.getItem('create_game');
if (from_localstorage === null) {
@@ -100,7 +101,7 @@
{#if data !== undefined}
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
<Editor bind:data />
<Editor bind:data bind:quiz_id />
</form>
{/if}
+1 -1
View File
@@ -122,7 +122,7 @@
{:then _}
{#if data !== undefined}
<form on:submit|preventDefault={submit} class="grid grid-cols-1 gap-2">
<Editor bind:data submit_button_text={$t('words.save')} />
<Editor bind:data submit_button_text={$t('words.save')} bind:quiz_id />
</form>
{/if}
{:catch err}