Merge branch 'master' into order
This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
# Workaround: https://github.com/docker/build-push-action/issues/461
|
||||
- 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
|
||||
# https://github.com/docker/login-action
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- "frontend/**"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -15,11 +16,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.0.0
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.9.3
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -28,6 +28,7 @@ from classquiz.routers import (
|
||||
login,
|
||||
sitemap,
|
||||
remote,
|
||||
community,
|
||||
)
|
||||
from classquiz.socket_server import sio
|
||||
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(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(community.router, tags=["community"], prefix="/api/v1/community", include_in_schema=True)
|
||||
|
||||
app.mount("/", ASGIApp(sio))
|
||||
|
||||
+6
-6
@@ -43,12 +43,12 @@ class OAuth2PasswordBearerWithCookie(OAuth2):
|
||||
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization: str = request.cookies.get("access_token") # changed to accept access token from httpOnly Cookie
|
||||
if authorization is None:
|
||||
try:
|
||||
authorization = request.state.access_token
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
authorization = request.state.access_token
|
||||
except AttributeError:
|
||||
authorization: str = request.cookies.get(
|
||||
"access_token"
|
||||
) # changed to accept access token from httpOnly Cookie
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
|
||||
@@ -133,6 +133,7 @@ class QuizQuestion(BaseModel):
|
||||
|
||||
@validator("answers")
|
||||
def answers_not_none_if_abcd_type(cls, v, values):
|
||||
# print(values)
|
||||
if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer:
|
||||
raise ValueError("Answers can't be none if type is ABCD")
|
||||
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
|
||||
@@ -272,3 +273,12 @@ class GameInLobby(BaseModel):
|
||||
game_pin: str
|
||||
quiz_title: str
|
||||
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)
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
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.db.models import UserSession
|
||||
from classquiz.oauth import google, github
|
||||
from classquiz.config import settings
|
||||
|
||||
settings = settings()
|
||||
|
||||
router = APIRouter()
|
||||
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):
|
||||
rememberme_cookie = request.cookies.get("rememberme_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 = (
|
||||
await UserSession.objects.filter(session_key=rememberme_cookie)
|
||||
.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):
|
||||
response: Response = await call_next(request)
|
||||
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)
|
||||
await user_session.update(last_seen=datetime.now())
|
||||
request.state.access_token = f"Bearer {access_token}"
|
||||
request.cookies.pop("access_token")
|
||||
response: Response = await call_next(request)
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
)
|
||||
else:
|
||||
response: Response = await call_next(request)
|
||||
|
||||
@@ -43,7 +43,7 @@ async def log_user_in(user: User, request: Request, response: Response):
|
||||
value=f"Bearer {access_token}",
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=settings.access_token_expire_minutes * 60,
|
||||
max_age=60 * 60 * 24 * 365,
|
||||
)
|
||||
response.set_cookie(
|
||||
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}",
|
||||
httponly=True,
|
||||
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.status_code = 200
|
||||
|
||||
@@ -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
|
||||
@@ -18,7 +18,7 @@ import bleach
|
||||
|
||||
from classquiz.auth import get_current_user
|
||||
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
|
||||
import html
|
||||
import urllib.parse
|
||||
@@ -75,17 +75,28 @@ 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)
|
||||
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):
|
||||
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)
|
||||
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:
|
||||
return JSONResponse(status_code=404, content={"detail": "quiz not found"})
|
||||
else:
|
||||
return quiz
|
||||
return PublicQuizResponse(**quiz.dict())
|
||||
|
||||
|
||||
@router.post("/start/{quiz_id}")
|
||||
|
||||
+33
-35
@@ -15,23 +15,22 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@beyonk/svelte-mapbox": "^8.2.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^35.3.2",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^35.3.2",
|
||||
"@ckeditor/ckeditor5-build-balloon": "^35.3.2",
|
||||
"@ckeditor/ckeditor5-editor-balloon": "^35.3.2",
|
||||
"@ckeditor/ckeditor5-essentials": "^35.3.2",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^35.3.2",
|
||||
"@felte/reporter-tippy": "^1.1.4",
|
||||
"@felte/validator-yup": "^1.0.10",
|
||||
"@ckeditor/ckeditor5-autoformat": "^35.4.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^35.4.0",
|
||||
"@ckeditor/ckeditor5-build-balloon": "^35.4.0",
|
||||
"@ckeditor/ckeditor5-editor-balloon": "^35.4.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^35.4.0",
|
||||
"@ckeditor/ckeditor5-theme-lark": "^35.4.0",
|
||||
"@felte/reporter-tippy": "^1.1.5",
|
||||
"@felte/validator-yup": "^1.0.11",
|
||||
"@fontsource/marck-script": "^4.5.11",
|
||||
"@neodrag/svelte": "^1.2.4",
|
||||
"@sentry/browser": "^7.23.0",
|
||||
"@sentry/tracing": "^7.23.0",
|
||||
"@sentry/browser": "^7.31.1",
|
||||
"@sentry/tracing": "^7.31.1",
|
||||
"@simplewebauthn/browser": "^6.2.2",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/adapter-node": "^1.0.0-next.101",
|
||||
"@sveltejs/kit": "^1.0.1",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@sveltejs/adapter-auto": "^1.0.2",
|
||||
"@sveltejs/adapter-node": "^1.1.4",
|
||||
"@sveltejs/kit": "^1.2.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
@@ -39,8 +38,8 @@
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"@uppy/compressor": "^1.0.1",
|
||||
"@uppy/core": "^3.0.4",
|
||||
"@uppy/dashboard": "^3.2.0",
|
||||
@@ -56,46 +55,45 @@
|
||||
"cookie": "^0.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"cssnano": "^5.1.14",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"felte": "^1.2.6",
|
||||
"felte": "^1.2.7",
|
||||
"fuse.js": "^6.6.2",
|
||||
"highlight.js": "^11.7.0",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"luxon": "^3.1.1",
|
||||
"mapbox-gl": "^2.11.0",
|
||||
"luxon": "^3.2.1",
|
||||
"mapbox-gl": "^2.12.0",
|
||||
"mdsvex": "^0.10.6",
|
||||
"minisearch": "^5.1.0",
|
||||
"pikaso": "^2.7.4",
|
||||
"plausible-tracker": "^0.3.8",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"sass": "^1.56.1",
|
||||
"sass": "^1.57.1",
|
||||
"socket.io-client": "^4.5.4",
|
||||
"sortablejs": "^1.15.0",
|
||||
"svelte": "^3.53.1",
|
||||
"svelte-check": "^2.10.0",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"svelte-range-slider-pips": "^2.1.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^3.0.2",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"svelte-range-slider-pips": "^2.1.1",
|
||||
"svelte-tippy": "^1.3.2",
|
||||
"swiper": "^8.4.5",
|
||||
"swiper": "^8.4.6",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "~4.7.4",
|
||||
"ua-parser-js": "^1.0.32",
|
||||
"vite": "^4.0.1",
|
||||
"ua-parser-js": "^1.0.33",
|
||||
"vite": "^4.0.4",
|
||||
"vite-plugin-iso-import": "^1.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"i18next": "^22.0.6"
|
||||
"i18next": "^22.4.9"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+558
-577
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,12 @@
|
||||
.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];
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import ca from './locales/ca.json';
|
||||
import it from './locales/it.json';
|
||||
import es from './locales/es.json';
|
||||
import nb_no from './locales/nb_NO.json';
|
||||
import zh_Hant from './locales/zh_Hant.json';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import type { i18n, Resource } from 'i18next';
|
||||
@@ -68,6 +69,7 @@ export class I18nService {
|
||||
this.i18n.addResourceBundle('ca', 'translation', ca);
|
||||
this.i18n.addResourceBundle('es', 'translation', es);
|
||||
this.i18n.addResourceBundle('nb_NO', 'translation', nb_no);
|
||||
this.i18n.addResourceBundle('zh_Hant', 'translation', zh_Hant);
|
||||
}
|
||||
|
||||
changeLanguage(language: string): void {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"see_all_quizzes": "Sieh all deine Quizze",
|
||||
"students_site": "Seite der Schüler*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).",
|
||||
"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",
|
||||
@@ -92,7 +92,8 @@
|
||||
"unexpected": "Der gute alte unerwartete Fehler hat uns heimgesucht!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"use_backup_code": "Backup-Code benutzen"
|
||||
},
|
||||
"words": {
|
||||
"question": "Frage",
|
||||
@@ -150,7 +151,10 @@
|
||||
"practice": "Üben",
|
||||
"error": "Fehler",
|
||||
"voting": "Umfrage",
|
||||
"download": "Herunterladen"
|
||||
"download": "Herunterladen",
|
||||
"continue": "Weiter",
|
||||
"totp": "TOTP",
|
||||
"backup_code": "Backup-Code"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Zeit in Sekunden",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"features_description": {
|
||||
"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!",
|
||||
"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.",
|
||||
"see_what_true_and_false": "Ver lo que estaba correcto o incorrecto",
|
||||
@@ -16,20 +16,20 @@
|
||||
"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",
|
||||
"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",
|
||||
"german_server": "Servidor alemán",
|
||||
"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",
|
||||
"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",
|
||||
"create_or_import": "Crear o importar",
|
||||
"see_all_quizzes": "Ver todos tus cuestionarios",
|
||||
"teachers_site": "Portal de profesores",
|
||||
"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",
|
||||
"view_results": "Ver los resultados",
|
||||
"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",
|
||||
"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!",
|
||||
"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)",
|
||||
"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.",
|
||||
"play_quiz": "2. Haz el quiz",
|
||||
"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!",
|
||||
"self_hostable": "Autohospedable"
|
||||
"no_tracking_content": "Kahoot! rastrea y comparte su perfil con terceros, pero ClassQuiz no lo hace.",
|
||||
"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": {
|
||||
"success": {
|
||||
"body": "¡Creación del quiz con éxito!",
|
||||
"title": "¡Quiz creado con éxito!"
|
||||
"body": "Que empiecen los juegos.",
|
||||
"title": "Cuestionario creado."
|
||||
}
|
||||
},
|
||||
"login_page": {
|
||||
"modal": {
|
||||
"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": {
|
||||
"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": "¡Se ha iniciado la sesión con éxito!"
|
||||
"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": "Conectado."
|
||||
},
|
||||
"success": "¡Inicio de sesión exitoso!"
|
||||
"success": "Conectado."
|
||||
},
|
||||
"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!",
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"welcome_back": "¡Bienvenido de nuevo!",
|
||||
"login_or_create_account": "Iniciar sesión o crear una cuenta",
|
||||
"already_have_account": "¿No tienes una cuenta?"
|
||||
"welcome_back": "Bienvenido de nuevo.",
|
||||
"login_or_create_account": "Ingresar o Crear una cuenta",
|
||||
"already_have_account": "¿No tienes una cuenta?",
|
||||
"use_backup_code": "Usar el código de la copia de seguridad"
|
||||
},
|
||||
"overview_page": {
|
||||
"created_at": "Creado en",
|
||||
"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": {
|
||||
"success_update_title": "¡Quiz actualizado con éxito!",
|
||||
"success_update_body": "¡Actualización del quiz con éxito!"
|
||||
"success_update_title": "Cuestionario actualizado.",
|
||||
"success_update_body": "Nadie esperará la Inquisición."
|
||||
},
|
||||
"register_page": {
|
||||
"greeting": "¡Encantado de conocerte!",
|
||||
@@ -114,8 +119,8 @@
|
||||
"answer": "Respuesta",
|
||||
"stats": "Estadísticas",
|
||||
"features": "Características",
|
||||
"login": "Inicio de sesión",
|
||||
"email": "Correo electrónico",
|
||||
"login": "Iniciar sesión",
|
||||
"email": "Dirección de correo electrónico",
|
||||
"username": "Nombre de usuario",
|
||||
"count": "Cuenta",
|
||||
"range": "Zona",
|
||||
@@ -145,7 +150,11 @@
|
||||
"correct": "Correcto",
|
||||
"result": "Resultado",
|
||||
"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": {
|
||||
"export_results": "Exportar resultados",
|
||||
@@ -157,7 +166,7 @@
|
||||
"start_game": "Iniciar el juego",
|
||||
"time_left": "Tiempo restante",
|
||||
"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!"
|
||||
},
|
||||
"settings_page": {
|
||||
@@ -183,7 +192,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"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}}."
|
||||
},
|
||||
"error_page": {
|
||||
@@ -201,19 +210,29 @@
|
||||
},
|
||||
"import_page": {
|
||||
"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": {
|
||||
"made_by": "Hecho por",
|
||||
"imported_by": "Importado por"
|
||||
},
|
||||
"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": {
|
||||
"reset_password": "Restablecer contraseña"
|
||||
},
|
||||
"dashboard": {
|
||||
"search_for_own_quizzes": "Busca tus propios quizzes"
|
||||
},
|
||||
"uploader": {
|
||||
"add_image": "Añadir una imagen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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! 匯入測驗。"
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,11 @@
|
||||
code: 'nb_NO',
|
||||
name: 'Norsk',
|
||||
flag: '🇳🇴'
|
||||
},
|
||||
{
|
||||
code: 'zh_Hant',
|
||||
name: 'Chinese (traditional)',
|
||||
flag: '🇨🇳'
|
||||
}
|
||||
];
|
||||
const get_selected_language = (): string => {
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<img
|
||||
alt="QR code to join the game"
|
||||
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>
|
||||
<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">
|
||||
<p>{@html quiz.description}</p>
|
||||
</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}
|
||||
<div class="flex justify-center align-middle items-center">
|
||||
<div class="h-[15vh] m-auto w-auto my-3">
|
||||
|
||||
Reference in New Issue
Block a user