Merge branch 'master' into order

This commit is contained in:
Mawoka
2023-01-26 21:13:48 +01:00
committed by GitHub
21 changed files with 1183 additions and 664 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Install the cosign tool except on PR # Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer # https://github.com/sigstore/cosign-installer
@@ -43,7 +43,7 @@ jobs:
# Workaround: https://github.com/docker/build-push-action/issues/461 # Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf uses: docker/setup-buildx-action@v2.2.1
# Login against a Docker registry except on PR # Login against a Docker registry except on PR
# https://github.com/docker/login-action # https://github.com/docker/login-action
+3 -2
View File
@@ -8,6 +8,7 @@ on:
branches: [ master ] branches: [ master ]
paths: paths:
- "frontend/**" - "frontend/**"
workflow_dispatch:
pull_request: pull_request:
jobs: jobs:
@@ -15,11 +16,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2.0.0 - uses: actions/checkout@v3.3.0
with: with:
fetch-depth: 1 fetch-depth: 1
- uses: pnpm/action-setup@v2.2.2 - uses: pnpm/action-setup@v2.2.4
with: with:
version: 7.9.3 version: 7.9.3
working-directory: ./frontend working-directory: ./frontend
+2
View File
@@ -28,6 +28,7 @@ from classquiz.routers import (
login, login,
sitemap, sitemap,
remote, remote,
community,
) )
from classquiz.socket_server import sio from classquiz.socket_server import sio
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
@@ -98,5 +99,6 @@ app.include_router(
app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True) app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True)
app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True) app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True)
app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True) app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True)
app.include_router(community.router, tags=["community"], prefix="/api/v1/community", include_in_schema=True)
app.mount("/", ASGIApp(sio)) app.mount("/", ASGIApp(sio))
+6 -6
View File
@@ -43,12 +43,12 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]: async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie try:
if authorization is None: authorization = request.state.access_token
try: except AttributeError:
authorization = request.state.access_token authorization: str = request.cookies.get(
except AttributeError: "access_token"
pass ) # changed to accept access token from httpOnly Cookie
scheme, param = get_authorization_scheme_param(authorization) scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer": if not authorization or scheme.lower() != "bearer":
if self.auto_error: if self.auto_error:
+10
View File
@@ -133,6 +133,7 @@ class QuizQuestion(BaseModel):
@validator("answers") @validator("answers")
def answers_not_none_if_abcd_type(cls, v, values): def answers_not_none_if_abcd_type(cls, v, values):
# print(values)
if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer: if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer:
raise ValueError("Answers can't be none if type is ABCD") raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer: if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
@@ -272,3 +273,12 @@ class GameInLobby(BaseModel):
game_pin: str game_pin: str
quiz_title: str quiz_title: str
game_id: uuid.UUID game_id: uuid.UUID
#
# class UserProfileLinks(ormar.Model):
# id: int = ormar.Integer(primary_key=True, autoincrement=True)
# user: Optional[User] = ormar.ForeignKey(User)
# github_username: str | None = ormar.Text(nullable=True)
# reddit_username: str | None = ormar.Text(nullable=True)
# kahoot_user_id: str | None = ormar.Text(nullable=True)
+47 -3
View File
@@ -4,11 +4,15 @@
from datetime import timedelta, datetime from datetime import timedelta, datetime
from fastapi import APIRouter, Request, Response from fastapi import APIRouter, Request, Response
from fastapi.security.utils import get_authorization_scheme_param
from jose import jws, jwt, JWTError, JWSError
from classquiz.auth import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token from classquiz.auth import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token
from classquiz.db.models import UserSession from classquiz.db.models import UserSession
from classquiz.oauth import google, github from classquiz.oauth import google, github
from classquiz.config import settings
settings = settings()
router = APIRouter() router = APIRouter()
router.include_router(google.router, prefix="/google") router.include_router(google.router, prefix="/google")
@@ -18,7 +22,45 @@ router.include_router(github.router, prefix="/github")
async def rememberme_middleware(request: Request, call_next): async def rememberme_middleware(request: Request, call_next):
rememberme_cookie = request.cookies.get("rememberme_token") rememberme_cookie = request.cookies.get("rememberme_token")
bearer_token = request.cookies.get("access_token") bearer_token = request.cookies.get("access_token")
if rememberme_cookie is not None and bearer_token is None: conditions_to_handle_met = True
# print(bearer_token)
# if bearer_token is not None:
# bearer_token = bearer_token.replace("Bearer ", "")
# test = jws.verify(bearer_token, settings.secret_key, algorithms=["HS256"])
# try:
# jwt.decode(bearer_token, settings.secret_key, algorithms=["HS256"])
# print("jwt ok")
# except JWTError as e:
# print("jwt failed")
# print(test)
scheme, param = get_authorization_scheme_param(bearer_token)
# if bearer token is none, we can just do the request, since you can't be signed in
if scheme is None or param is None:
conditions_to_handle_met = False
# if rememberme token is none, we can just do the request, since you can't be signed in
if rememberme_cookie is None:
conditions_to_handle_met = False
if scheme.lower() != "bearer":
conditions_to_handle_met = False
# Verifying the bearer
try:
jwt.decode(
param, settings.secret_key, algorithms=["HS256"]
) # checking if the token is valid, throws error if not
conditions_to_handle_met = False
except JWTError:
try:
jws.verify(
param, settings.secret_key, algorithms=["HS256"]
) # Verifying only the signature of the jwt, throws error if signature is invalid
except JWSError:
conditions_to_handle_met = False
if conditions_to_handle_met:
user_session: UserSession | None = ( user_session: UserSession | None = (
await UserSession.objects.filter(session_key=rememberme_cookie) await UserSession.objects.filter(session_key=rememberme_cookie)
.select_related(UserSession.user) .select_related(UserSession.user)
@@ -27,17 +69,19 @@ async def rememberme_middleware(request: Request, call_next):
if (user_session is None) or (user_session.user is None): if (user_session is None) or (user_session.user is None):
response: Response = await call_next(request) response: Response = await call_next(request)
return response return response
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES * 60) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# access_token_expires = timedelta(seconds=1)
access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires) access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires)
await user_session.update(last_seen=datetime.now()) await user_session.update(last_seen=datetime.now())
request.state.access_token = f"Bearer {access_token}" request.state.access_token = f"Bearer {access_token}"
request.cookies.pop("access_token")
response: Response = await call_next(request) response: Response = await call_next(request)
response.set_cookie( response.set_cookie(
key="access_token", key="access_token",
value=f"Bearer {access_token}", value=f"Bearer {access_token}",
httponly=True, httponly=True,
samesite="lax", samesite="lax",
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60, max_age=60 * 60 * 24 * 365,
) )
else: else:
response: Response = await call_next(request) response: Response = await call_next(request)
+2 -2
View File
@@ -43,7 +43,7 @@ async def log_user_in(user: User, request: Request, response: Response):
value=f"Bearer {access_token}", value=f"Bearer {access_token}",
httponly=True, httponly=True,
samesite="lax", samesite="lax",
max_age=settings.access_token_expire_minutes * 60, max_age=60 * 60 * 24 * 365,
) )
response.set_cookie( response.set_cookie(
key="rememberme_token", value=session_key, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 365 key="rememberme_token", value=session_key, httponly=True, samesite="lax", max_age=60 * 60 * 24 * 365
@@ -64,7 +64,7 @@ async def rememberme_check(rememberme_token: str, response: Response):
value=f"Bearer {access_token}", value=f"Bearer {access_token}",
httponly=True, httponly=True,
samesite="lax", samesite="lax",
max_age=settings.access_token_expire_minutes * 60, max_age=60 * 60 * 24 * 365,
) )
response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60) response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60)
response.status_code = 200 response.status_code = 200
+34
View File
@@ -0,0 +1,34 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from fastapi import APIRouter, HTTPException
from uuid import UUID
from classquiz.db.models import User, Quiz
router = APIRouter()
#
@router.get("/user/{user_id}", response_model_include={"username", "created_at", "id"}, response_model=User)
async def get_user_by_user_id(user_id: UUID):
user = await User.objects.get_or_none(id=user_id)
# .select_related("quizs")
# print(user)
if user is None:
raise HTTPException(status_code=404, detail="user not found")
else:
return user
@router.get("/quizzes/{user_id}", response_model_exclude={"questions", "user_id"}, response_model=list[Quiz])
async def get_quizzes_from_user(user_id: UUID, imported: bool | None = None):
if imported is None:
quizzes = await Quiz.objects.all(user_id=user_id, public=True)
else:
quizzes = await Quiz.objects.all(user_id=user_id, public=True, imported_from_kahoot=imported)
if len(quizzes) == 0:
raise HTTPException(status_code=404, detail="no quizzes found")
else:
return quizzes
+15 -4
View File
@@ -18,7 +18,7 @@ import bleach
from classquiz.auth import get_current_user from classquiz.auth import get_current_user
from classquiz.config import redis, settings, storage, meilisearch from classquiz.config import redis, settings, storage, meilisearch
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby, QuizQuestion
from classquiz.kahoot_importer.import_quiz import import_quiz from classquiz.kahoot_importer.import_quiz import import_quiz
import html import html
import urllib.parse import urllib.parse
@@ -75,17 +75,28 @@ async def get_quiz_from_id(quiz_id: str, user: User | None = Depends(get_current
return quiz return quiz
@router.get("/get/public/{quiz_id}", response_model=Quiz) class PublicQuizResponseUser(BaseModel):
username: str
id: uuid.UUID
class PublicQuizResponse(Quiz.get_pydantic()):
user_id: PublicQuizResponseUser
questions: list[QuizQuestion]
@router.get("/get/public/{quiz_id}")
async def get_public_quiz(quiz_id: str): async def get_public_quiz(quiz_id: str):
try: try:
quiz_id = uuid.UUID(quiz_id) quiz_id = uuid.UUID(quiz_id)
except ValueError: except ValueError:
raise HTTPException(status_code=400, detail="badly formed quiz id") raise HTTPException(status_code=400, detail="badly formed quiz id")
quiz = await Quiz.objects.get_or_none(id=quiz_id) quiz = await Quiz.objects.select_related("user_id").get_or_none(id=quiz_id)
print(quiz.dict(exclude={"user_id": {"avatar"}}))
if quiz is None: if quiz is None:
return JSONResponse(status_code=404, content={"detail": "quiz not found"}) return JSONResponse(status_code=404, content={"detail": "quiz not found"})
else: else:
return quiz return PublicQuizResponse(**quiz.dict())
@router.post("/start/{quiz_id}") @router.post("/start/{quiz_id}")
+33 -35
View File
@@ -15,23 +15,22 @@
}, },
"devDependencies": { "devDependencies": {
"@beyonk/svelte-mapbox": "^8.2.0", "@beyonk/svelte-mapbox": "^8.2.0",
"@ckeditor/ckeditor5-autoformat": "^35.3.2", "@ckeditor/ckeditor5-autoformat": "^35.4.0",
"@ckeditor/ckeditor5-basic-styles": "^35.3.2", "@ckeditor/ckeditor5-basic-styles": "^35.4.0",
"@ckeditor/ckeditor5-build-balloon": "^35.3.2", "@ckeditor/ckeditor5-build-balloon": "^35.4.0",
"@ckeditor/ckeditor5-editor-balloon": "^35.3.2", "@ckeditor/ckeditor5-editor-balloon": "^35.4.0",
"@ckeditor/ckeditor5-essentials": "^35.3.2", "@ckeditor/ckeditor5-essentials": "^35.4.0",
"@ckeditor/ckeditor5-theme-lark": "^35.3.2", "@ckeditor/ckeditor5-theme-lark": "^35.4.0",
"@felte/reporter-tippy": "^1.1.4", "@felte/reporter-tippy": "^1.1.5",
"@felte/validator-yup": "^1.0.10", "@felte/validator-yup": "^1.0.11",
"@fontsource/marck-script": "^4.5.11", "@fontsource/marck-script": "^4.5.11",
"@neodrag/svelte": "^1.2.4", "@sentry/browser": "^7.31.1",
"@sentry/browser": "^7.23.0", "@sentry/tracing": "^7.31.1",
"@sentry/tracing": "^7.23.0",
"@simplewebauthn/browser": "^6.2.2", "@simplewebauthn/browser": "^6.2.2",
"@sveltejs/adapter-auto": "^1.0.0", "@sveltejs/adapter-auto": "^1.0.2",
"@sveltejs/adapter-node": "^1.0.0-next.101", "@sveltejs/adapter-node": "^1.1.4",
"@sveltejs/kit": "^1.0.1", "@sveltejs/kit": "^1.2.2",
"@tailwindcss/typography": "^0.5.8", "@tailwindcss/typography": "^0.5.9",
"@types/canvas-confetti": "^1.6.0", "@types/canvas-confetti": "^1.6.0",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
@@ -39,8 +38,8 @@
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/sortablejs": "^1.15.0", "@types/sortablejs": "^1.15.0",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.48.2",
"@uppy/compressor": "^1.0.1", "@uppy/compressor": "^1.0.1",
"@uppy/core": "^3.0.4", "@uppy/core": "^3.0.4",
"@uppy/dashboard": "^3.2.0", "@uppy/dashboard": "^3.2.0",
@@ -56,46 +55,45 @@
"cookie": "^0.5.0", "cookie": "^0.5.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"cssnano": "^5.1.14", "cssnano": "^5.1.14",
"eslint": "^8.28.0", "eslint": "^8.32.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"felte": "^1.2.6", "felte": "^1.2.7",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"luxon": "^3.1.1", "luxon": "^3.2.1",
"mapbox-gl": "^2.11.0", "mapbox-gl": "^2.12.0",
"mdsvex": "^0.10.6", "mdsvex": "^0.10.6",
"minisearch": "^5.1.0", "minisearch": "^5.1.0",
"pikaso": "^2.7.4", "pikaso": "^2.7.4",
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.8",
"postcss": "^8.4.19", "postcss": "^8.4.21",
"postcss-import": "^14.1.0", "postcss-import": "^14.1.0",
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"prettier": "^2.8.0", "prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.9.0",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"sass": "^1.56.1", "sass": "^1.57.1",
"socket.io-client": "^4.5.4", "socket.io-client": "^4.5.4",
"sortablejs": "^1.15.0", "svelte": "^3.55.1",
"svelte": "^3.53.1", "svelte-check": "^3.0.2",
"svelte-check": "^2.10.0", "svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.0", "svelte-range-slider-pips": "^2.1.1",
"svelte-range-slider-pips": "^2.1.0",
"svelte-tippy": "^1.3.2", "svelte-tippy": "^1.3.2",
"swiper": "^8.4.5", "swiper": "^8.4.6",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"ua-parser-js": "^1.0.32", "ua-parser-js": "^1.0.33",
"vite": "^4.0.1", "vite": "^4.0.4",
"vite-plugin-iso-import": "^1.0.0", "vite-plugin-iso-import": "^1.0.0",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"i18next": "^22.0.6" "i18next": "^22.4.9"
} }
} }
+558 -577
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -26,7 +26,12 @@
.normal-background { .normal-background {
@apply bg-gradient-to-r from-[#009444] via-[#39b54a] to-[#8dc63f] dark:bg-[#0f2702] dark:from-[#0f2702] dark:via-[#0f2702] dark:to[#0f2702]; @apply bg-gradient-to-r from-[#009444] via-[#39b54a] to-[#8dc63f] dark:bg-[#0f2702] dark:from-[#0f2702] dark:via-[#0f2702] dark:to[#0f2702];
} }
.admin-button { .admin-button {
@apply px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50; @apply px-4 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded text-center hover:bg-gray-600 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50;
} }
.action-button {
@apply px-4 py-2 leading-5 text-black dark:text-white transition-colors duration-200 transform bg-gray-50 dark:bg-gray-700 rounded text-center hover:bg-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-gray-600;
}
} }
+2
View File
@@ -15,6 +15,7 @@ import ca from './locales/ca.json';
import it from './locales/it.json'; import it from './locales/it.json';
import es from './locales/es.json'; import es from './locales/es.json';
import nb_no from './locales/nb_NO.json'; import nb_no from './locales/nb_NO.json';
import zh_Hant from './locales/zh_Hant.json';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import type { i18n, Resource } from 'i18next'; import type { i18n, Resource } from 'i18next';
@@ -68,6 +69,7 @@ export class I18nService {
this.i18n.addResourceBundle('ca', 'translation', ca); this.i18n.addResourceBundle('ca', 'translation', ca);
this.i18n.addResourceBundle('es', 'translation', es); this.i18n.addResourceBundle('es', 'translation', es);
this.i18n.addResourceBundle('nb_NO', 'translation', nb_no); this.i18n.addResourceBundle('nb_NO', 'translation', nb_no);
this.i18n.addResourceBundle('zh_Hant', 'translation', zh_Hant);
} }
changeLanguage(language: string): void { changeLanguage(language: string): void {
+7 -3
View File
@@ -17,7 +17,7 @@
"see_all_quizzes": "Sieh all deine Quizze", "see_all_quizzes": "Sieh all deine Quizze",
"students_site": "Seite der Schüler*innen", "students_site": "Seite der Schüler*innen",
"teachers_site": "Seite der Lehrer*innen", "teachers_site": "Seite der Lehrer*innen",
"no_tracking_content": "Kahoot! trackt mit mindestens 2 amerikanischen Drittanbietern und ClassQuiz trackt gar nicht mit Drittanbietern!", "no_tracking_content": "Kahoot! trackt mit mindestens zwei amerikanischen Drittanbietern und ClassQuiz trackt gar nicht mit Drittanbietern!",
"quiz_results_downloadable_content": "Quiz-Ergebnisse können einfach in Form einer Excel-Tabelle heruntergeladen werden (Wusste gar nicht, dass andere dies nicht können).", "quiz_results_downloadable_content": "Quiz-Ergebnisse können einfach in Form einer Excel-Tabelle heruntergeladen werden (Wusste gar nicht, dass andere dies nicht können).",
"multilingual_content": "ClassQuiz ist jetzt schon komplett in Englisch, Deutsch, Türkisch, Französisch, norwegischem Bokmål und Italienisch verfügbar, wobei es auch teilweise in Indonesisch und Katalanisch verfügbar ist.", "multilingual_content": "ClassQuiz ist jetzt schon komplett in Englisch, Deutsch, Türkisch, Französisch, norwegischem Bokmål und Italienisch verfügbar, wobei es auch teilweise in Indonesisch und Katalanisch verfügbar ist.",
"no_tracking": "Kein Tracking", "no_tracking": "Kein Tracking",
@@ -92,7 +92,8 @@
"unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!" "unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!"
} }
} }
} },
"use_backup_code": "Backup-Code benutzen"
}, },
"words": { "words": {
"question": "Frage", "question": "Frage",
@@ -150,7 +151,10 @@
"practice": "Üben", "practice": "Üben",
"error": "Fehler", "error": "Fehler",
"voting": "Umfrage", "voting": "Umfrage",
"download": "Herunterladen" "download": "Herunterladen",
"continue": "Weiter",
"totp": "TOTP",
"backup_code": "Backup-Code"
}, },
"editor": { "editor": {
"time_in_seconds": "Zeit in Sekunden", "time_in_seconds": "Zeit in Sekunden",
+48 -29
View File
@@ -8,7 +8,7 @@
"features_description": { "features_description": {
"1": "ClassQuiz es una plataforma de quiz que permite crear y gestionar quiz.", "1": "ClassQuiz es una plataforma de quiz que permite crear y gestionar quiz.",
"2": "La principal funcionalidad es una función de importación de Kahoot! que permite importar cuestionarios de Kahoot!", "2": "La principal funcionalidad es una función de importación de Kahoot! que permite importar cuestionarios de Kahoot!",
"3": "Destaca el editor sencillo, así como la función de exportar los resultados de los cuestionarios a archivos de Excel." "3": "El editor fácil de usar es un punto destacado, al igual que la función de exportación para descargar los resultados de las pruebas como archivos de Excel."
}, },
"stats": "Ya hay {{user_count}} usuarios y {{quiz_count}} cuestionarios en ClassQuiz.", "stats": "Ya hay {{user_count}} usuarios y {{quiz_count}} cuestionarios en ClassQuiz.",
"see_what_true_and_false": "Ver lo que estaba correcto o incorrecto", "see_what_true_and_false": "Ver lo que estaba correcto o incorrecto",
@@ -16,20 +16,20 @@
"get_a_quiz": "1. Haz un quiz", "get_a_quiz": "1. Haz un quiz",
"create_a_quiz_from_scratch": "Crea un quiz desde cero con el editor e incluye imágenes y más", "create_a_quiz_from_scratch": "Crea un quiz desde cero con el editor e incluye imágenes y más",
"find_or_explore": "Encuentra (o explora) quizzes hechos o importados por otras personas", "find_or_explore": "Encuentra (o explora) quizzes hechos o importados por otras personas",
"import_quiz_from_kahoot_and_edit": "Importa un cuestionario de Kahoot! y edítalo en ClassQuiz", "import_quiz_from_kahoot_and_edit": "¡Importa un cuestionario de Kahoot! y editarlo en ClassQuiz",
"no_tracking": "Sin rastreo", "no_tracking": "Sin rastreo",
"german_server": "Servidor alemán", "german_server": "Servidor alemán",
"user_friendly": "Fácil de usar", "user_friendly": "Fácil de usar",
"completely_free": "Totalmente gratuito", "completely_free": "Totalmente gratis",
"quiz_results_downloadable": "Los resultados de los cuestionarios se pueden descargar", "quiz_results_downloadable": "Los resultados de los cuestionarios se pueden descargar",
"multilingual": "Multilingüe", "multilingual": "Multilingüe",
"completely_free_content": "ClassQuiz es completamente gratuito, sin planes de pago ni molestas redirecciones a la página de actualización. Sin embargo, ¡se agradece una donación!", "completely_free_content": "ClassQuiz es gratuito para el usuario, sin planes pagos ni redireccionamientos para una versión paga. Por lo tanto, cualquier donación es apreciada.",
"see_how_many_true_and_false": "Ver cuántos estaban correctos o equivocados", "see_how_many_true_and_false": "Ver cuántos estaban correctos o equivocados",
"create_or_import": "Crear o importar", "create_or_import": "Crear o importar",
"see_all_quizzes": "Ver todos tus cuestionarios", "see_all_quizzes": "Ver todos tus cuestionarios",
"teachers_site": "Portal de profesores", "teachers_site": "Portal de profesores",
"students_site": "Portal estudiante", "students_site": "Portal estudiante",
"multilingual_content": "ClassQuiz ya está totalmente traducido al inglés, alemán, turco, francés e italiano. También está parcialmente traducido al indonesio y al catalán.", "multilingual_content": "ClassQuiz ya está completamente disponible en inglés, alemán, turco, francés, bokmål noruego e italiano, mientras que también está disponible parcialmente en indonesio y catalán.",
"select_answer": "Selecciona la respuesta", "select_answer": "Selecciona la respuesta",
"view_results": "Ver los resultados", "view_results": "Ver los resultados",
"check_if_chosen_wisely": "Comprueba, si has elegido bien", "check_if_chosen_wisely": "Comprueba, si has elegido bien",
@@ -37,52 +37,57 @@
"get_ranking_and_winners": "Consigue la clasificación y mira quién ha ganado", "get_ranking_and_winners": "Consigue la clasificación y mira quién ha ganado",
"why_classquiz": "¿Por qué ClassQuiz?", "why_classquiz": "¿Por qué ClassQuiz?",
"self_hostable_content": "ClassQuiz puede ser fácilmente auto-alojado, por lo que los datos sólo están bajo tu control!", "self_hostable_content": "ClassQuiz puede ser fácilmente auto-alojado, por lo que los datos sólo están bajo tu control!",
"user_friendly_content": "ClassQuiz trata de ser lo más fácil de uso posible, por lo que es fácil de usar para todo el mundo", "user_friendly_content": "ClassQuiz está diseñado para ser simple y fácil de usar para todos.",
"quiz_results_downloadable_content": "Los resultados de los cuestionarios se pueden exportar fácilmente a una hoja de cálculo de Excel. (No sabía que otros no pudieran hacerlo)", "quiz_results_downloadable_content": "Los resultados de los cuestionarios se pueden exportar fácilmente a una hoja de cálculo de Excel. (No sabía que otros no pudieran hacerlo)",
"dark_mode_content": "Una de las funcionalidades más importantes que puede tener un sitio web!", "dark_mode_content": "Una de las funcionalidades más importantes que puede tener un sitio web!",
"german_server_content": "Los servidores de ClassQuiz se encuentran en Alemania y están alojados con netcup.", "german_server_content": "Los servidores de ClassQuiz se encuentran en Alemania y están alojados con netcup.",
"play_quiz": "2. Haz el quiz", "play_quiz": "2. Haz el quiz",
"choose_answer_wisely": "Elige bien tu respuesta", "choose_answer_wisely": "Elige bien tu respuesta",
"no_tracking_content": "Kahoot! rastrea con al menos 2 proveedores americanos y ClassQuiz no rastrea con ningún proveedor!", "no_tracking_content": "Kahoot! rastrea y comparte su perfil con terceros, pero ClassQuiz no lo hace.",
"self_hostable": "Autohospedable" "self_hostable": "Autohospedable",
"download_quizzes": "Descargar cuestionarios",
"community_driven_content": "¡ClassQuiz depende de la comunidad que proporciona ClassQuiz con donaciones, solicitudes de funciones, traducciones y más! ¡También puede convertirse en parte de la comunidad de ClassQuiz!",
"community_driven": "Impulsado por la comunidad",
"download_quizzes_content": "Los cuestionarios se pueden descargar como un solo archivo e importar en cualquier momento, lo que le permite mover fácilmente sus cuestionarios a otra instancia de ClassQuiz."
}, },
"create_page": { "create_page": {
"success": { "success": {
"body": "¡Creación del quiz con éxito!", "body": "Que empiecen los juegos.",
"title": "¡Quiz creado con éxito!" "title": "Cuestionario creado."
} }
}, },
"login_page": { "login_page": {
"modal": { "modal": {
"success": { "success": {
"success_check_mail": "¡Inicio de sesión exitoso! ¡Comprueba tu buzón de correo!", "success_check_mail": "Conectado. Por favor revise su bandeja de entrada de correo electrónico.",
"description": { "description": {
"success_check_mail": "Por favor, comprueba tu buzón, ya que deberías haber recibido un correo, con un enlace en el que puedes hacer clic para iniciar sesión.", "success_check_mail": "Verifique su buzón de correo ya que debería haber recibido un correo electrónico con un enlace para iniciar sesión.",
"success": "¡Se ha iniciado la sesión con éxito!" "success": "Conectado."
}, },
"success": "¡Inicio de sesión exitoso!" "success": "Conectado."
}, },
"error": { "error": {
"wrong_creds": "Correo electrónico o contraseña incorrectos!", "wrong_creds": "Dirección de correo electrónico o contraseña incorrecta.",
"unexpected": "¡Error inesperado!", "unexpected": "¡Error inesperado!",
"description": { "description": {
"wrong_creds": "Asegúrate de que tu contraseña y tu correo electrónico son correctos!", "wrong_creds": "Por favor, asegúrese de que su contraseña y dirección de correo electrónico sean correctas.",
"unexpected": "Se produjo el típico error inesperado!" "unexpected": "Se produjo el típico error inesperado!"
} }
} }
}, },
"welcome_back": "¡Bienvenido de nuevo!", "welcome_back": "Bienvenido de nuevo.",
"login_or_create_account": "Iniciar sesión o crear una cuenta", "login_or_create_account": "Ingresar o Crear una cuenta",
"already_have_account": "¿No tienes una cuenta?" "already_have_account": "¿No tienes una cuenta?",
"use_backup_code": "Usar el código de la copia de seguridad"
}, },
"overview_page": { "overview_page": {
"created_at": "Creado en", "created_at": "Creado en",
"question_count": "Recuento de preguntas", "question_count": "Recuento de preguntas",
"no_quizzes": "Parece que no tienes ningún quiz. ¿Quieres cambiar eso? ¡Haz clic en el botón \"Crear\" o importa un cuestionario de Kahoot!" "no_quizzes": "Haga clic en el botón \"Crear\" o importe un cuestionario de Kahoot. para ponerse en marcha."
}, },
"edit_page": { "edit_page": {
"success_update_title": "¡Quiz actualizado con éxito!", "success_update_title": "Cuestionario actualizado.",
"success_update_body": "¡Actualización del quiz con éxito!" "success_update_body": "Nadie esperará la Inquisición."
}, },
"register_page": { "register_page": {
"greeting": "¡Encantado de conocerte!", "greeting": "¡Encantado de conocerte!",
@@ -114,8 +119,8 @@
"answer": "Respuesta", "answer": "Respuesta",
"stats": "Estadísticas", "stats": "Estadísticas",
"features": "Características", "features": "Características",
"login": "Inicio de sesión", "login": "Iniciar sesión",
"email": "Correo electrónico", "email": "Dirección de correo electrónico",
"username": "Nombre de usuario", "username": "Nombre de usuario",
"count": "Cuenta", "count": "Cuenta",
"range": "Zona", "range": "Zona",
@@ -145,7 +150,11 @@
"correct": "Correcto", "correct": "Correcto",
"result": "Resultado", "result": "Resultado",
"result_plural": "Resultados", "result_plural": "Resultados",
"password": "Contraseña" "password": "Contraseña",
"download": "Descargar",
"continue": "Continuar",
"backup_code": "Código de la copia de seguridad",
"totp": "contraseña de un solo uso (Totp)"
}, },
"admin_page": { "admin_page": {
"export_results": "Exportar resultados", "export_results": "Exportar resultados",
@@ -157,7 +166,7 @@
"start_game": "Iniciar el juego", "start_game": "Iniciar el juego",
"time_left": "Tiempo restante", "time_left": "Tiempo restante",
"get_final_results": "Ver los resultados finales", "get_final_results": "Ver los resultados finales",
"start_by_showing_first_question": "Comienza mostrando la primera pregunta!", "start_by_showing_first_question": "Comienza mostrando la primera pregunta.",
"no_answers": "¡No hay respuestas!" "no_answers": "¡No hay respuestas!"
}, },
"settings_page": { "settings_page": {
@@ -183,7 +192,7 @@
}, },
"footer": { "footer": {
"self_ads": "Hecho con ❤️ por {{mawoka_link}} y con la ayuda de {{others_link}}.", "self_ads": "Hecho con ❤️ por {{mawoka_link}} y con la ayuda de {{others_link}}.",
"more_details_here": "Más detalles aquí", "more_details_here": "Para más información",
"donate": "Si te resulta útil, considera {{donate_link}}." "donate": "Si te resulta útil, considera {{donate_link}}."
}, },
"error_page": { "error_page": {
@@ -201,19 +210,29 @@
}, },
"import_page": { "import_page": {
"need_help": "¿Necesitas ayuda?", "need_help": "¿Necesitas ayuda?",
"visit_docs": "Visite la documentación" "visit_docs": "Visite la documentación",
"url_should_look_like_this": "La URL debería verse así: https://create.kahoot.it/details/...",
"a_kahoot_quiz": "¡Un Kahoot! Prueba",
"side_import_kahoot": "¡En este lado puedes importar cuestionarios, que viven en Kahoot!.",
"classquiz_quiz": "Un ClassQuiz-Quiz",
"upload_file_ending": "Cargue el archivo que termina en .cqa",
"this_side_classquiz": "Desde aquí puede importar cuestionarios exportados desde ClassQuiz."
}, },
"explore_page": { "explore_page": {
"made_by": "Hecho por", "made_by": "Hecho por",
"imported_by": "Importado por" "imported_by": "Importado por"
}, },
"search_page": { "search_page": {
"at_least_3_characters": "Escribe al menos 3 caracteres..." "at_least_3_characters": "Escribe al menos 3 caracteres...",
"nothing_here": "No hay nada aquí..."
}, },
"password_reset_page": { "password_reset_page": {
"reset_password": "Restablecer contraseña" "reset_password": "Restablecer contraseña"
}, },
"dashboard": { "dashboard": {
"search_for_own_quizzes": "Busca tus propios quizzes" "search_for_own_quizzes": "Busca tus propios quizzes"
},
"uploader": {
"add_image": "Añadir una imagen"
} }
} }
+194
View File
@@ -0,0 +1,194 @@
{
"index_page": {
"meta": {
"title": "首頁",
"description": "ClassQuiz 是一款為學生設計的似 Kahoot! 測驗應用程式,不僅開源還免費"
},
"slogan": "開源的測驗平台!",
"create_or_import": "建立或匯入",
"see_all_quizzes": "查看你所有的測驗",
"teachers_site": "教師頁面",
"students_site": "學生頁面",
"no_tracking": "零追蹤",
"self_hostable": "可自行架設",
"german_server": "德國伺服器",
"user_friendly": "易於使用",
"completely_free": "完全免費",
"multilingual": "多語言",
"dark_mode": "深色模式",
"get_a_quiz": "1. 取得測驗",
"create_a_quiz_from_scratch": "使用編輯器建立測驗並加入圖片和更多內容",
"import_quiz_from_kahoot_and_edit": "從 Kahoot! 匯入測驗並在 ClassQuiz 上進行編輯",
"play_quiz": "2. 遊玩測驗",
"select_answer": "選擇答案",
"choose_answer_wisely": "明智地選擇你的答案",
"view_results": "檢視結果",
"check_if_chosen_wisely": "驗證你的選擇是否正確",
"list_winners": "列出贏家",
"get_ranking_and_winners": "取得排名和贏家",
"why_classquiz": "為什麼選擇 ClassQuiz",
"download_quizzes": "下載測驗",
"stats": "ClassQuiz 上已有 {{user_count}} 位使用者和 {{quiz_count}} 個測驗。",
"features_description": {
"1": "ClassQuiz 是一個可以讓你建立和管理測驗的測驗平台。",
"2": "最主要的功能之一是允許你匯入 Kahoot! 上的測驗。"
},
"quiz_results_downloadable": "可被下載的測驗結果",
"find_or_explore": "搜尋 (或瀏覽) 其他人建立和匯入的測驗",
"no_tracking_content": "Kahoot! 會追蹤並將你的資料分享給第三方,但 ClassQuiz 不會。",
"user_friendly_content": "ClassQuiz 旨在簡單,每個人都可以輕鬆使用。",
"dark_mode_content": "一個網站最重要的功能之一!",
"german_server_content": "ClassQuiz 的伺服器位於德國,並由 netcup 提供。"
},
"edit_page": {
"success_update_title": "測驗已更新。"
},
"create_page": {
"success": {
"title": "測驗已建立。",
"body": "讓遊戲開始吧。"
}
},
"register_page": {
"create_account": "建立帳戶",
"forgot_password?": "忘記密碼?",
"already_have_account?": "已經擁有帳戶?",
"greeting": "很高興見到你!"
},
"login_page": {
"login_or_create_account": "登入或建立帳戶",
"already_have_account": "沒有帳戶?",
"modal": {
"error": {
"wrong_creds": "電子郵件地址或密碼錯誤。",
"unexpected": "非預期錯誤!",
"description": {
"wrong_creds": "請確認你的密碼和電子郵件地址是否正確。"
}
},
"success": {
"success": "已登入。",
"success_check_mail": "已登入。請檢查你的電子信箱收件匣。",
"description": {
"success": "已登入。"
}
}
},
"welcome_back": "歡迎回來。",
"use_backup_code": "使用備用代碼"
},
"editor": {
"delete_question": "刪除問題",
"add_new_question": "新增問題",
"delete_answer": "刪除答案",
"add_new_answer": "新增答案"
},
"import_page": {
"need_help": "需要幫助?"
},
"admin_page": {
"start_game": "開始遊戲",
"get_results": "取得結果",
"get_results_and_stop_time": "取得結果並停止計時",
"get_final_results": "取得最終結果",
"export_results": "匯出結果",
"show_next_question": "顯示下一個問題",
"time_left": "剩餘時間",
"stop_time": "停止計時"
},
"password_reset_page": {
"reset_password": "重設密碼"
},
"settings_page": {
"old_password": "舊密碼",
"new_password": "新密碼",
"repeat_password": "再次輸入密碼",
"change_password_submit": "變更密碼!",
"check_location": "檢查位置"
},
"explore_page": {
"made_by": "作者為",
"imported_by": "匯入者為"
},
"play_page": {
"2nd_place": "第二名",
"3rd place": "第三名",
"with_out_of": "{{total_question_count}} 題中答對 {{correct_questions}} 題",
"1st_place": "第一名"
},
"editor_page": {
"add_an_answer": "新增一個答案",
"right_click_to_delete": "右鍵點擊一個答案即可將它刪除!"
},
"footer": {
"more_details_here": "更多詳細資訊"
},
"uploader": {
"add_image": "新增圖片"
},
"words": {
"find": "尋找",
"question": "問題",
"answer": "答案",
"stats": "統計資料",
"features": "功能",
"login": "登入",
"email": "電子郵件地址",
"username": "使用者名稱",
"password": "密碼",
"edit": "編輯",
"delete": "刪除",
"public": "公開",
"create": "建立",
"import": "匯入",
"logout": "登出",
"title": "標題",
"url": "URL",
"submit": "提交",
"pin": "PIN",
"kick": "踢出",
"register": "註冊",
"close": "關閉",
"save": "儲存",
"description": "描述",
"image": "圖片",
"settings": "設定",
"repeat_password": "再次輸入密碼",
"explore": "發現",
"screenshot": "螢幕截圖",
"screenshot_plural": "螢幕截圖",
"browser": "瀏覽器",
"correct": "正確",
"result": "結果",
"result_plural": "結果",
"range": "範圍",
"multiple_choice": "選擇題",
"private": "私人",
"dashboard": "儀錶板",
"question_plural": "問題",
"game_pin": "遊戲 PIN",
"other": "其他",
"search": "搜尋",
"other_plural": "其他",
"download": "下載",
"voting": "投票",
"docs": "文件",
"donating": "贊助",
"practice": "練習",
"error": "錯誤",
"continue": "繼續",
"backup_code": "備用代碼",
"totp": ""
},
"search_page": {
"at_least_3_characters": "輸入至少 3 個字元..."
},
"dashboard": {
"search_for_own_quizzes": "搜尋你的測驗"
},
"overview_page": {
"created_at": "建立於",
"question_count": "問題數",
"no_quizzes": "點擊「建立」按鈕或從 Kahoot! 匯入測驗。"
}
}
+5
View File
@@ -97,6 +97,11 @@
code: 'nb_NO', code: 'nb_NO',
name: 'Norsk', name: 'Norsk',
flag: '🇳🇴' flag: '🇳🇴'
},
{
code: 'zh_Hant',
name: 'Chinese (traditional)',
flag: '🇨🇳'
} }
]; ];
const get_selected_language = (): string => { const get_selected_language = (): string => {
+1 -1
View File
@@ -172,7 +172,7 @@
<img <img
alt="QR code to join the game" alt="QR code to join the game"
src="/api/v1/utils/qr/{quiz_data.game_pin}?dark_mode={bg_color ? false : darkMode}" src="/api/v1/utils/qr/{quiz_data.game_pin}?dark_mode={bg_color ? false : darkMode}"
class="block mx-auto w-1/6" class="block mx-auto w-1/6 mt-12"
/> />
<p class="text-3xl text-center ">{$t('words.pin')}: {quiz_data.game_pin}</p> <p class="text-3xl text-center ">{$t('words.pin')}: {quiz_data.game_pin}</p>
<div class="flex justify-center w-full mt-4"> <div class="flex justify-center w-full mt-4">
@@ -0,0 +1,182 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import type { PageData } from './$types';
import { getLocalization } from '$lib/i18n';
import { onMount } from 'svelte';
import { createTippy } from 'svelte-tippy';
import { signedIn } from '$lib/stores';
import StartGamePopup from '$lib/dashboard/start_game.svelte';
const { t } = getLocalization();
// import { DateTime } from 'luxon';
let start_game = null;
const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'right'
});
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
if (key.code === 'Escape') {
start_game = null;
}
};
onMount(() => {
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
});
export let data: PageData;
console.log(data, 'data');
</script>
<svelte:head>
<title>ClassQuiz - @{data.user.username}</title>
</svelte:head>
<div class="h-full">
<div class="grid grid-cols-6 h-full">
<div class="pl-2">
<div class="flex justify-center">
<img src={`/api/v1/users/avatar/${data.user.id}`} alt="profile" />
</div>
<h2 class="text-3xl text-center">
@{data.user.username}
</h2>
<p class="italic text-center">
Joined on {new Date(data.user.created_at).toLocaleDateString()}
</p>
</div>
<div
class="col-start-2 col-end-7 border-l border-black h-full p-4 overflow-y-scroll flex flex-col gap-4"
>
{#each data.quizzes as quiz}
<div
class="rounded-lg border-2 border-black hover:outline transition-all outline-[#B07156] -outline-offset-2 outline-8"
>
<div class="grid grid-cols-6 h-[25vh]">
<!-- <p
style='writing-mode: vertical-lr'
class='text-center h-full text-xl p-2'
>
{@html quiz.title}
</p>-->
<div class="col-start-2 col-end-6">
<h3 class="text-center text-2xl">{@html quiz.title}</h3>
<p class="text-center">
{@html quiz.description}
</p>
{#if quiz.cover_image}
<div class="flex justify-center align-middle items-center">
<div class="h-[20vh] m-auto w-auto max-h-[18vh]">
<img
class="max-h-full max-w-full block"
src={quiz.cover_image}
alt="Not provided"
loading="lazy"
/>
</div>
</div>
{/if}
</div>
<!-- <div class='flex justify-end'>
<p
style='writing-mode: sideways-lr'
class='text-center text-xl p-2'
>
{@html quiz.title}
</p>
</div>-->
</div>
<div class="flex justify-center">
<a href="/view/{quiz.id}" class="action-button w-1/6">{$t('words.view')}</a>
</div>
<div class="flex justify-center pb-10 pt-8">
<div class="grid grid-cols-2 gap-3 w-1/3">
{#if $signedIn}
<button
on:click={() => {
start_game = quiz.id;
}}
class="action-button"
>
{$t('words.start')}
</button>
<a
href="/api/v1/eximport/{quiz.id}"
aria-label="Download the quiz"
class="flex justify-center px-4 py-2 leading-5 text-black dark:text-white transition-colors duration-200 transform bg-gray-50 dark:bg-gray-700 rounded text-center hover:bg-gray-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-gray-600"
><!-- heroicons/download -->
<svg
class="w-5 h-5"
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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</a>
{:else}
<div
use:tippy={{
content: 'You need be signed in to start a game.'
}}
class="w-full"
>
<button
on:click={() => {
start_game = quiz.id;
}}
disabled
class="action-button w-full"
>
{$t('words.start')}
</button>
</div>
<div
use:tippy={{
content: 'You need be signed in to download a game.'
}}
class="w-full"
>
<button
disabled
class="action-button w-full flex justify-center"
>
<svg
class="w-5 h-5"
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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
</button>
</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
</div>
</div>
{#if start_game !== null}
<StartGamePopup bind:quiz_id={start_game} />
{/if}
@@ -0,0 +1,24 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// import type { PageLoad } from './$types';
export const load = async ({ params, fetch }) => {
const user_req = await fetch(`/api/v1/community/user/${params.user_id}`);
const user = await user_req.json();
if (!user) {
return {
user: undefined,
quizzes: undefined
};
}
const quiz_req = await fetch(`/api/v1/community/quizzes/${params.user_id}?imported=false`);
const quizzes = await quiz_req.json();
return {
user,
quizzes
};
}; // satisfies PageLoad;
@@ -68,6 +68,9 @@
<div class="text-center"> <div class="text-center">
<p>{@html quiz.description}</p> <p>{@html quiz.description}</p>
</div> </div>
<p class="text-center">
Made by <a href="/user/{quiz.user_id.id}" class="underline">@{quiz.user_id.username}</a>
</p>
{#if quiz.cover_image} {#if quiz.cover_image}
<div class="flex justify-center align-middle items-center"> <div class="flex justify-center align-middle items-center">
<div class="h-[15vh] m-auto w-auto my-3"> <div class="h-[15vh] m-auto w-auto my-3">