diff --git a/Caddyfile b/Caddyfile
index 5e4ff90..2a36f17 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -5,6 +5,7 @@
reverse_proxy /openapi.json localhost:8000
reverse_proxy /socket.io* localhost:8000
reverse_proxy /socket.io/* localhost:8000
+ reverse_proxy /ms http://localhost:7700
}
diff --git a/Pipfile b/Pipfile
index dc9527d..fb202e1 100644
--- a/Pipfile
+++ b/Pipfile
@@ -22,6 +22,7 @@ jinja2 = "*"
argon2-cffi = "*"
sentry-sdk = "*"
aiofiles = "*"
+meilisearch = "*"
[dev-packages]
coverage = "*"
@@ -32,3 +33,6 @@ black = "*"
[requires]
python_version = "3.10"
+
+[scripts]
+format = "black ."
\ No newline at end of file
diff --git a/Pipfile.lock b/Pipfile.lock
index 1018ce8..e11c0a0 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "075e07880b28878645b61e807c6091ae0ecdef50b3486e047aa853023fc2ae33"
+ "sha256": "7e2c8532f63f702ec73822fc0a4cab8fdf54de46dcd6be01aee4fbd0e5a8937e"
},
"pipfile-spec": 6,
"requires": {
@@ -299,7 +299,7 @@
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
- "markers": "python_version >= '3.5'",
+ "markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
@@ -544,7 +544,7 @@
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
- "markers": "python_version >= '3.5'",
+ "markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"jinja2": {
@@ -609,6 +609,14 @@
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
+ "meilisearch": {
+ "hashes": [
+ "sha256:437037dec8730d047e790e94d9af43b59b076fa7511593739aae857eafd83a1b",
+ "sha256:907986a37b8072b3e27b19b4ee2bb7a1a6f4c9fae961f240aaceadec3adbbab2"
+ ],
+ "index": "pypi",
+ "version": "==0.18.2"
+ },
"multidict": {
"hashes": [
"sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60",
@@ -831,11 +839,11 @@
},
"pyparsing": {
"hashes": [
- "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
- "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
+ "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
+ "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.7"
+ "markers": "python_full_version >= '3.6.8'",
+ "version": "==3.0.8"
},
"python-dotenv": {
"hashes": [
@@ -928,6 +936,14 @@
"index": "pypi",
"version": "==4.2.2"
},
+ "requests": {
+ "hashes": [
+ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
+ "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+ "version": "==2.27.1"
+ },
"rsa": {
"hashes": [
"sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17",
@@ -938,19 +954,19 @@
},
"sentry-sdk": {
"hashes": [
- "sha256:32af1a57954576709242beb8c373b3dbde346ac6bd616921def29d68846fb8c3",
- "sha256:38fd16a92b5ef94203db3ece10e03bdaa291481dd7e00e77a148aa0302267d47"
+ "sha256:02c0f79941964297341c14d53e4df09a81c949707bd91e2d5e66b565aad75c8b",
+ "sha256:afd9f92711556739172f3bc845a02fc921ca1560bee974ae65072ef6a30e3921"
],
"index": "pypi",
- "version": "==1.5.8"
+ "version": "==1.5.9"
},
"setuptools": {
"hashes": [
- "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a",
- "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967"
+ "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8",
+ "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592"
],
"markers": "python_version >= '3.7'",
- "version": "==62.0.0"
+ "version": "==62.1.0"
},
"six": {
"hashes": [
@@ -965,7 +981,7 @@
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
- "markers": "python_version >= '3.5'",
+ "markers": "python_full_version >= '3.5.0'",
"version": "==1.2.0"
},
"sqlalchemy": {
@@ -1454,11 +1470,11 @@
},
"pyparsing": {
"hashes": [
- "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
- "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
+ "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
+ "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
- "markers": "python_version >= '3.6'",
- "version": "==3.0.7"
+ "markers": "python_full_version >= '3.6.8'",
+ "version": "==3.0.8"
},
"pytest": {
"hashes": [
@@ -1482,7 +1498,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
- "markers": "python_version < '3.11'",
+ "markers": "python_version >= '3.7'",
"version": "==2.0.1"
}
}
diff --git a/classquiz/__init__.py b/classquiz/__init__.py
index f91d596..8749237 100644
--- a/classquiz/__init__.py
+++ b/classquiz/__init__.py
@@ -5,7 +5,7 @@ from socketio import ASGIApp
from classquiz.config import settings
from classquiz.db import database
-from classquiz.routers import users, quiz, utils, stats, storage
+from classquiz.routers import users, quiz, utils, stats, storage, search
from classquiz.socket_server import sio
settings = settings()
@@ -46,4 +46,5 @@ 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.mount("/", ASGIApp(sio))
diff --git a/classquiz/config.py b/classquiz/config.py
index 0cde720..e93f769 100644
--- a/classquiz/config.py
+++ b/classquiz/config.py
@@ -2,6 +2,7 @@ from functools import lru_cache
import redis.asyncio as redis_lib
from pydantic import BaseSettings, RedisDsn, PostgresDsn
+import meilisearch as MeiliSearch
from classquiz.storage import Storage
@@ -25,6 +26,8 @@ class Settings(BaseSettings):
access_token_expire_minutes: int = 30
cache_expiry: int = 86400
sentry_dsn: str | None = "https://4981de1f72f24fd7b5e21b8913b93a02@o661934.ingest.sentry.io/6254641"
+ meilisearch_url: str
+ meilisearch_index: str = "classquiz"
# storage_backend
storage_backend: str | None = "deta"
@@ -52,3 +55,5 @@ storage: Storage = Storage(
deta_id=settings().deta_project_id,
storage_path=settings().storage_path,
)
+
+meilisearch = MeiliSearch.Client(settings().meilisearch_url)
diff --git a/classquiz/db/models.py b/classquiz/db/models.py
index a91c962..3da892b 100644
--- a/classquiz/db/models.py
+++ b/classquiz/db/models.py
@@ -1,5 +1,6 @@
import uuid
from datetime import datetime
+from typing import Optional
import ormar
from pydantic import BaseModel, Json
@@ -32,7 +33,7 @@ class UserSession(ormar.Model):
"""
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4())
- user: uuid.UUID = ormar.ForeignKey(User)
+ user: Optional[User] = ormar.ForeignKey(User)
session_key: str = ormar.String(unique=True, max_length=64)
created_at: datetime = ormar.DateTime(default=datetime.now())
ip_address: str = ormar.String(max_length=100, nullable=True)
@@ -71,7 +72,7 @@ class Quiz(ormar.Model):
description: str = ormar.String(max_length=300, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now())
updated_at: datetime = ormar.DateTime(default=datetime.now())
- user_id: uuid.UUID = ormar.UUID(foreign_key=User.id)
+ user_id: uuid.UUID = ormar.ForeignKey(User)
questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False)
class Meta:
diff --git a/classquiz/kahoot_importer/import_quiz.py b/classquiz/kahoot_importer/import_quiz.py
index 8eb43d4..28eb5ec 100644
--- a/classquiz/kahoot_importer/import_quiz.py
+++ b/classquiz/kahoot_importer/import_quiz.py
@@ -5,7 +5,7 @@ from datetime import datetime
from aiohttp import ClientSession
-from classquiz.config import settings, storage
+from classquiz.config import settings, storage, meilisearch
from classquiz.db.models import Quiz, QuizAnswer, QuizQuestion, User
from classquiz.kahoot_importer.get import get as get_quiz
@@ -88,7 +88,7 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
)
quiz_data = Quiz(
id=quiz_id,
- public=False,
+ public=True,
title=quiz.kahoot.title,
description=quiz.kahoot.description,
created_at=datetime.now(),
@@ -96,4 +96,14 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str:
user_id=user.id,
questions=json.dumps(quiz_questions),
)
+ meilisearch.index(settings.meilisearch_index).add_documents(
+ [
+ {
+ "id": str(quiz_data.id),
+ "title": quiz_data.title,
+ "description": quiz_data.description,
+ "user": (await User.objects.filter(id=quiz_data.user_id).first()).username,
+ }
+ ]
+ )
return await quiz_data.save()
diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py
index 4ba7a51..6cd39df 100644
--- a/classquiz/routers/quiz.py
+++ b/classquiz/routers/quiz.py
@@ -8,14 +8,13 @@ from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from pydantic import ValidationError
-from classquiz.auth import get_current_user, get_current_user_optional
-from classquiz.config import redis, settings, storage
+from classquiz.auth import get_current_user
+from classquiz.config import redis, settings, storage, meilisearch
from classquiz.db.models import Quiz, QuizInput, User, PlayGame
from classquiz.kahoot_importer.import_quiz import import_quiz
settings = settings()
-
router = APIRouter()
@@ -31,11 +30,21 @@ async def create_quiz_lol(quiz_input: QuizInput, user: User = Depends(get_curren
raise HTTPException(status_code=400, detail="image url is not valid")
quiz = Quiz(**quiz_input.dict(), user_id=user.id, id=uuid.uuid4())
await redis.delete("global_quiz_count")
+ meilisearch.index(settings.meilisearch_index).add_documents(
+ [
+ {
+ "id": str(quiz.id),
+ "title": quiz.title,
+ "description": quiz.description,
+ "user": (await User.objects.filter(id=quiz.user_id).first()).username,
+ }
+ ]
+ )
return await quiz.save()
@router.get("/get/{quiz_id}")
-async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user_optional)):
+async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current_user)):
try:
quiz_id = uuid.UUID(quiz_id)
except ValueError:
@@ -54,6 +63,19 @@ async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current
return quiz
+@router.get("/get/public/{quiz_id}", response_model=Quiz)
+async def get_public_quiz(quiz_id: str):
+ try:
+ quiz_id = uuid.UUID(quiz_id)
+ except ValueError:
+ raise HTTPException(status_code=400, detail="badly formed quiz id")
+ quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True)
+ if quiz is None:
+ return JSONResponse(status_code=404, content={"detail": "quiz not found"})
+ else:
+ return quiz
+
+
@router.post("/start/{quiz_id}")
async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)):
try:
@@ -62,7 +84,9 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)):
raise HTTPException(status_code=400, detail="badly formed quiz id")
quiz = await Quiz.objects.get_or_none(id=quiz_id, user_id=user.id)
if quiz is None:
- return JSONResponse(status_code=404, content={"detail": "quiz not found"})
+ quiz = await Quiz.objects.get_or_none(id=quiz_id, public=True)
+ if quiz is None:
+ return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else:
game_pin = randint(10000000, 99999999)
game = PlayGame(
@@ -112,11 +136,36 @@ async def update_quiz(quiz_id: str, quiz_input: QuizInput, user: User = Depends(
else:
# print(quiz_input)
# print(quiz)
+ meilisearch.index(settings.meilisearch_index).update_documents(
+ [
+ {
+ "id": str(quiz.id),
+ "title": quiz_input.title,
+ "description": quiz_input.description,
+ "user": (await User.objects.filter(id=quiz.user_id).first()).username,
+ }
+ ]
+ )
+ if quiz.public and not quiz_input.public:
+ print("removing from meilisearch")
+ 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(
+ [
+ {
+ "id": str(quiz.id),
+ "title": quiz_input.title,
+ "description": quiz_input.description,
+ "user": (await User.objects.filter(id=quiz.user_id).first()).username,
+ }
+ ]
+ )
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.update()
@@ -149,4 +198,5 @@ async def delete_quiz(quiz_id: str, user: User = Depends(get_current_user)):
pass
if len(pics_to_delete) != 0:
await storage.delete(pics_to_delete)
+ meilisearch.index(settings.meilisearch_index).delete_document(str(quiz.id))
return await quiz.delete()
diff --git a/classquiz/routers/search.py b/classquiz/routers/search.py
new file mode 100644
index 0000000..99c6c5a
--- /dev/null
+++ b/classquiz/routers/search.py
@@ -0,0 +1,88 @@
+import pydantic
+from fastapi import APIRouter
+
+from classquiz.config import settings, meilisearch
+from uuid import UUID
+from typing import Optional, List
+
+settings = settings()
+
+router = APIRouter()
+
+
+class Hit(pydantic.BaseModel):
+ id: UUID
+ title: str
+ description: str
+ user: str
+ formatted: Optional["Hit"] = pydantic.Field(None, alias="_formatted")
+
+
+class SearchResponse(pydantic.BaseModel):
+ hits: List[Hit] | list[None]
+ nbHits: int
+ exhaustiveNbHits: bool
+ query: str
+ limit: int
+ offset: int
+ processingTimeMs: int
+
+
+class SearchData(pydantic.BaseModel):
+ q: str
+ offset: Optional[int] = 0
+ limit: Optional[int] = 20
+ filter: Optional[str] = None
+ facetsDistribution: Optional[list[str]] = None
+ attributesToRetrieve: Optional[list[str]] = ["*"]
+ attributesToCrop: Optional[list[str]] = None
+ cropLength: Optional[int] = 200
+ attributesToHighlight: Optional[list[str]] = None
+ matches: Optional[bool] = False
+ sort: Optional[list[str]] = None
+
+
+@router.post("/", response_model=SearchResponse)
+async def search(data: SearchData):
+ index = meilisearch.get_index(settings.meilisearch_index)
+ query = index.search(
+ data.q,
+ {
+ "offset": data.offset,
+ "limit": data.limit,
+ "filter": data.filter,
+ "cropLength": data.cropLength,
+ "matches": data.matches,
+ "facetsDistribution": data.facetsDistribution,
+ "attributesToRetrieve": data.attributesToRetrieve,
+ "attributesToCrop": data.attributesToCrop,
+ "sort": data.sort,
+ "attributesToHighlight": data.attributesToHighlight,
+ },
+ )
+ return SearchResponse(**query)
+
+
+@router.get("/", response_model=SearchResponse)
+async def search_get(
+ q: str,
+ offset: int = 0,
+ limit: int = 20,
+ filter: str | None = None, # skipcq: PYL-W0622
+ cropLength: int = 200,
+ matches: bool = False,
+ attributesToHighlight: Optional[str] = "*",
+):
+ index = meilisearch.get_index(settings.meilisearch_index)
+ query = index.search(
+ q,
+ {
+ "offset": offset,
+ "limit": limit,
+ "filter": filter, # skipcq: PYL-W0622
+ "cropLength": cropLength,
+ "matches": matches,
+ "attributesToHighlight": [attributesToHighlight],
+ },
+ )
+ return SearchResponse(**query)
diff --git a/classquiz/scripts/__init__.py b/classquiz/scripts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.yml b/docker-compose.yml
index b33fe8b..f4ef6d4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -59,5 +59,13 @@ services:
- ./Caddyfile-docker:/etc/caddy/Caddyfile
ports:
- "8000:8080"
+ meilisearch:
+ image: getmeili/meilisearch:latest
+ restart: always
+ environment:
+ MEILI_NO_ANALYTICS: true
+ volumes:
+ - meilisearch-data:/data.ms
volumes:
- data:
\ No newline at end of file
+ data:
+ meilisearch-data:
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 3ae7142..d9856f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,9 +50,10 @@
"svelte": "^3.47.0",
"svelte-check": "^2.6.0",
"svelte-preprocess": "^4.10.5",
- "sveltejs-tippy": "^3.0.0",
+ "svelte-tippy": "^1.3.2",
"swiper": "^8.1.0",
"tailwindcss": "^3.0.24",
+ "tippy.js": "^6.3.7",
"tslib": "^2.3.1",
"typescript": "~4.6",
"yup": "^0.32.11"
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 9089db5..d65661f 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -38,9 +38,10 @@ specifiers:
svelte: ^3.47.0
svelte-check: ^2.6.0
svelte-preprocess: ^4.10.5
- sveltejs-tippy: ^3.0.0
+ svelte-tippy: ^1.3.2
swiper: ^8.1.0
tailwindcss: ^3.0.24
+ tippy.js: ^6.3.7
tslib: ^2.3.1
typescript: ~4.6
yup: ^0.32.11
@@ -55,7 +56,7 @@ devDependencies:
'@sentry/tracing': 6.19.6
'@sveltejs/adapter-auto': 1.0.0-next.34
'@sveltejs/adapter-node': 1.0.0-next.73
- '@sveltejs/kit': 1.0.0-next.311_sass@1.50.0+svelte@3.47.0
+ '@sveltejs/kit': 1.0.0-next.312_sass@1.50.0+svelte@3.47.0
'@tailwindcss/typography': 0.5.2_tailwindcss@3.0.24
'@types/cookie': 0.4.1
'@types/js-cookie': 3.0.1
@@ -85,9 +86,10 @@ devDependencies:
svelte: 3.47.0
svelte-check: 2.6.0_1b6d41eb44819e98e49be8cacdc96c52
svelte-preprocess: 4.10.5_f6bc70e70cf1a2fde5bc22fd7000ea23
- sveltejs-tippy: 3.0.0
+ svelte-tippy: 1.3.2
swiper: 8.1.0
tailwindcss: 3.0.24
+ tippy.js: 6.3.7
tslib: 2.3.1
typescript: 4.6.3
yup: 0.32.11
@@ -392,10 +394,10 @@ packages:
esbuild: 0.14.36
dev: true
- /@sveltejs/kit/1.0.0-next.311_sass@1.50.0+svelte@3.47.0:
+ /@sveltejs/kit/1.0.0-next.312_sass@1.50.0+svelte@3.47.0:
resolution:
{
- integrity: sha512-OkRIi0XBXcs16J+H+6fgOeFYxKu05gamY08KHAQ5cohjymUODGWIwaIfwAuNkZAcEEr8Vh2ejp4UcaxNzD53lQ==
+ integrity: sha512-l07ct0FJQnt+y3/NSEBvHKDKVE/7syYQnFhCVZ6H+4LEYRtTNybFxUTC8jA3xpZwPqJLZz1X+Tt+Sa8ZMuVzDg==
}
engines: { node: '>=14.13' }
hasBin: true
@@ -3507,6 +3509,15 @@ packages:
typescript: 4.6.3
dev: true
+ /svelte-tippy/1.3.2:
+ resolution:
+ {
+ integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==
+ }
+ dependencies:
+ tippy.js: 6.3.7
+ dev: true
+
/svelte/3.47.0:
resolution:
{
@@ -3515,15 +3526,6 @@ packages:
engines: { node: '>= 8' }
dev: true
- /sveltejs-tippy/3.0.0:
- resolution:
- {
- integrity: sha512-LAfQikm61AeqZW1hCFWUsmGEMmhMHDvrPV0JhbT38Ell9SqBjmFaY//+YKUxaJvx6nLMxGQdlcHA1rDg2Ml8tQ==
- }
- dependencies:
- tippy.js: 6.0.3
- dev: true
-
/svgo/2.8.0:
resolution:
{
@@ -3600,10 +3602,10 @@ packages:
globrex: 0.1.2
dev: true
- /tippy.js/6.0.3:
+ /tippy.js/6.3.7:
resolution:
{
- integrity: sha512-buQF6HugTA4YeSA/s9xBhu0ferEAx7sRSn45G+Juh3p+Dz3vEKfxtqdg6JUnHIWDCV/r0u8Lrobs9AB4dtYcUQ==
+ integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
}
dependencies:
'@popperjs/core': 2.11.5
diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts
index c44cce9..e69de29 100644
--- a/frontend/src/app.d.ts
+++ b/frontend/src/app.d.ts
@@ -1,44 +0,0 @@
-///
{quiz.description}
++ {$t('words.question')}: {question.question} +
+ + {#if question.image} + + {$t('words.image')}: +{question.time}
+ + {#each question.answers as answer, index_answer} ++ {$t('words.answer')}: {index_answer + 1} + {$t('words.question')}: {index_question + 1} +
++ {$t('words.answer')} + : {quiz.questions[index_question].answers[index_answer].answer} +
+ +