diff --git a/.env b/.env index b085629..e9d1c5f 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ EMAIL=d.leclerc.pro@gmail.com -DOMAIN=liquors.dleclerc.me -PROXY_URL=http://liquors-quiz-app:8000 \ No newline at end of file +DOMAIN=quiz.dleclerc.me +PROXY_URL=http://quiz-app:8000 \ No newline at end of file diff --git a/Apps/Client/package-lock.json b/Apps/Client/package-lock.json index de2f71e..813eb78 100644 --- a/Apps/Client/package-lock.json +++ b/Apps/Client/package-lock.json @@ -1,11 +1,11 @@ { - "name": "liquors-quiz--client", + "name": "quiz--client", "version": "latest", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "liquors-quiz--client", + "name": "quiz--client", "version": "latest", "dependencies": { "@emotion/react": "^11.11.3", @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^2.2.1", "http-proxy-middleware": "^2.0.6", "i18next": "^23.10.0", + "i18next-http-backend": "^2.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -29,6 +30,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -797,9 +799,17 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -2042,6 +2052,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6360,6 +6381,14 @@ "node": ">=10" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9259,6 +9288,14 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/i18next-http-backend": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.0.tgz", + "integrity": "sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -11531,6 +11568,44 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", diff --git a/Apps/Client/package.json b/Apps/Client/package.json index b918240..9575dcf 100644 --- a/Apps/Client/package.json +++ b/Apps/Client/package.json @@ -1,5 +1,5 @@ { - "name": "liquors-quiz--client", + "name": "quiz--client", "version": "latest", "private": true, "dependencies": { @@ -10,6 +10,7 @@ "@reduxjs/toolkit": "^2.2.1", "http-proxy-middleware": "^2.0.6", "i18next": "^23.10.0", + "i18next-http-backend": "^2.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", @@ -24,6 +25,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/Apps/Client/public/index.html b/Apps/Client/public/index.html index 120593f..da08207 100644 --- a/Apps/Client/public/index.html +++ b/Apps/Client/public/index.html @@ -5,12 +5,12 @@ - + - Liquors of the World + Quiz diff --git a/Apps/Client/public/manifest.json b/Apps/Client/public/manifest.json index 7ebc30c..af63d48 100644 --- a/Apps/Client/public/manifest.json +++ b/Apps/Client/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Liquors of the World", - "name": "Liquors of the World", + "short_name": "Quiz App", + "name": "Quiz App", "icons": [ { "src": "favicon.ico", diff --git a/Apps/Client/src/App.tsx b/Apps/Client/src/App.tsx index fad8c9a..e6e1a37 100644 --- a/Apps/Client/src/App.tsx +++ b/Apps/Client/src/App.tsx @@ -3,33 +3,48 @@ import './App.scss'; import HomePage from './pages/HomePage'; import QuizPage from './pages/QuizPage'; import ScoresPage from './pages/ScoresPage'; -import { BACKGROUND_URLS, DEBUG } from './config'; +import { DEBUG, SERVER_ROOT } from './config'; import TestPage from './pages/TestPage'; import AuthRoute from './routes/AuthRoute'; import LoadingOverlay from './components/overlays/LoadingOverlay'; import AnswerOverlay from './components/overlays/AnswerOverlay'; import { useEffect, useState } from 'react'; -import { useDispatch } from './hooks/redux'; -import { ping } from './actions/UserActions'; -import { getRandom } from './utils/array'; +import { useDispatch, useSelector } from './hooks/redux'; +import { ping } from './actions/AuthActions'; import Nav from './components/Nav'; import ErrorPage from './pages/ErrorPage'; -import { fetchVersion } from './actions/AppActions'; +import { updateVersion } from './actions/AppActions'; +import { fetchQuizNames } from './actions/DataActions'; +import { CallGetBackgroundUrl } from './calls/data/CallGetBackgroundUrl'; function App() { const [backgroundUrl, setBackgroundUrl] = useState(''); const dispatch = useDispatch(); - - // Check if user is logged in already - useEffect(() => { - setBackgroundUrl(`url(${getRandom(BACKGROUND_URLS)})`); + const quiz = useSelector((state) => state.quiz) + + useEffect(() => { dispatch(ping()); - dispatch(fetchVersion()); + dispatch(updateVersion()); + dispatch(fetchQuizNames()); }, []); + useEffect(() => { + if (quiz.name === null) { + return; + } + + new CallGetBackgroundUrl(quiz.name).execute() + .then(({ data: path }) => { + const url = `${SERVER_ROOT}${path}`; + setBackgroundUrl(`url(${url})`); + }) + .catch((err) => console.error(err)); + + }, [quiz.name]); + return (
diff --git a/Apps/Client/src/actions/AppActions.ts b/Apps/Client/src/actions/AppActions.ts index b21f561..dd17cee 100644 --- a/Apps/Client/src/actions/AppActions.ts +++ b/Apps/Client/src/actions/AppActions.ts @@ -1,31 +1,19 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; import { VersionData } from '../types/DataTypes'; import { CallGetVersion } from '../calls/quiz/CallGetVersion'; import { setVersion } from '../reducers/AppReducer'; +import { ThunkAPI, createServerAction } from './ServerActions'; -export const fetchVersion = createAsyncThunk( - 'app/version', - async (_, { dispatch, rejectWithValue }) => { - try { - const { data } = await new CallGetVersion().execute(); +export const updateVersion = createServerAction( + 'app/updateVersion', + async (_, { dispatch }: ThunkAPI) => { + const { data } = await new CallGetVersion().execute(); - if (!data) { - throw new Error('MISSING_DATA'); - } - - const { version } = data as VersionData; - - dispatch(setVersion(version)); + if (!data) { + throw new Error('MISSING_DATA'); + } - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } + const { version } = data as VersionData; - console.error(`Could not get app version: ${error}`); - return rejectWithValue(error); - } - } + dispatch(setVersion(version)); + }, ); \ No newline at end of file diff --git a/Apps/Client/src/actions/AuthActions.ts b/Apps/Client/src/actions/AuthActions.ts new file mode 100644 index 0000000..16b6615 --- /dev/null +++ b/Apps/Client/src/actions/AuthActions.ts @@ -0,0 +1,45 @@ +import { CallLogIn } from '../calls/auth/CallLogIn'; +import { LoginData, PingData, UserData } from '../types/DataTypes'; +import { CallPing } from '../calls/auth/CallPing'; +import { CallLogOut } from '../calls/auth/CallLogOut'; +import { createServerAction } from './ServerActions'; + +export const login = createServerAction( + 'auth/login', + async (args: LoginData) => { + const { quizId } = args; + const { data } = await new CallLogIn().execute(args); + + if (!data) { + throw new Error('MISSING_DATA'); + } + + const user = data as UserData; + + return { + username: user.username, + isAdmin: user.isAdmin, + quizId, + }; + }, +); + +export const logout = createServerAction( + 'auth/logout', + async () => { + await new CallLogOut().execute(); + }, +); + +export const ping = createServerAction( + 'auth/ping', + async () => { + const { data } = await new CallPing().execute(); + + if (!data) { + throw new Error('MISSING_DATA'); + } + + return data as PingData; + }, +); \ No newline at end of file diff --git a/Apps/Client/src/actions/DataActions.ts b/Apps/Client/src/actions/DataActions.ts new file mode 100644 index 0000000..38aebb2 --- /dev/null +++ b/Apps/Client/src/actions/DataActions.ts @@ -0,0 +1,94 @@ +import { CallGetQuizNames } from '../calls/data/CallGetQuizNames'; +import { CallGetQuestions } from '../calls/quiz/CallGetQuestions'; +import { CallGetScores } from '../calls/quiz/CallGetScores'; +import { CallGetStatus } from '../calls/quiz/CallGetStatus'; +import { CallGetVotes } from '../calls/quiz/CallGetVotes'; +import { Language, QuizName } from '../constants'; +import { RootState } from '../stores/store'; +import { StatusData, GroupedScoreData } from '../types/DataTypes'; +import { QuizJSON } from '../types/JSONTypes'; +import { ThunkAPI, createServerAction } from './ServerActions'; + +export const fetchQuizNames = createServerAction( + 'data/fetchQuizNames', + async () => { + const { data } = await new CallGetQuizNames().execute(); + + return data as string[]; + }, +); + +type FetchQuestionsActionArgs = { lang: Language, quizName: QuizName }; +export const fetchQuestions = createServerAction( + 'data/fetchQuestions', + async ({ lang, quizName }: FetchQuestionsActionArgs) => { + const { data } = await new CallGetQuestions(lang, quizName).execute(); + + return data as QuizJSON; + }, +); + +export const fetchStatus = createServerAction( + 'data/fetchStatus', + async (quizId: string) => { + const { data } = await new CallGetStatus(quizId).execute(); + + return data as StatusData; + }, +); + +export const fetchVotes = createServerAction( + 'data/fetchVotes', + async (quizId: string) => { + const { data } = await new CallGetVotes(quizId).execute(); + + return data as number[]; + }, +); + +export const fetchScores = createServerAction( + 'data/fetchScores', + async (quizId: string) => { + const { data } = await new CallGetScores(quizId).execute(); + + return data as GroupedScoreData; + }, +); + + + +type FetchQuizDataActionArgs = { quizId: string, quizName: QuizName, lang: Language }; +export const fetchQuizData = createServerAction( + 'data/fetchQuizData', + async ({ quizId, quizName, lang }: FetchQuizDataActionArgs, { dispatch, getState }: ThunkAPI) => { + const result = await Promise.all([ + dispatch(fetchQuestions({ lang, quizName })), + dispatch(fetchVotes(quizId)), + dispatch(fetchScores(quizId)), + dispatch(fetchStatus(quizId)), + ]); + + const someFetchActionFailed = result + .map(({ type }) => type) + .some(type => type.endsWith('/rejected')); + + if (someFetchActionFailed) { + throw new Error('DATA_FETCH'); + } + + const { quiz } = getState() as RootState; + const status = quiz.status.data as StatusData; + const votes = quiz.votes.data as number[]; + + // The current question index in the app corresponds to the first question + // the user hasn't answered yet, unless the player has answered all the + // questions already + const questionIndex = status.questionIndex; + const playerQuestionIndex = votes.length; + + if (playerQuestionIndex < questionIndex) { + return playerQuestionIndex; + } + return questionIndex; + }, +); \ No newline at end of file diff --git a/Apps/Client/src/actions/DatabaseActions.ts b/Apps/Client/src/actions/DatabaseActions.ts index f54036b..204dd9c 100644 --- a/Apps/Client/src/actions/DatabaseActions.ts +++ b/Apps/Client/src/actions/DatabaseActions.ts @@ -1,27 +1,12 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { logout } from './UserActions'; +import { logout } from './AuthActions'; import { CallDeleteDatabase } from '../calls/quiz/CallDeleteDatabase'; +import { ThunkAPI, createServerAction } from './ServerActions'; -export const deleteDatabase = createAsyncThunk( +export const deleteDatabase = createServerAction( 'database/delete', - async (_, { dispatch, rejectWithValue }) => { - try { - await new CallDeleteDatabase().execute(); + async (_, { dispatch }: ThunkAPI) => { + await new CallDeleteDatabase().execute(); - dispatch(logout()); - - return; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(error); - - return rejectWithValue(error); - } - } + dispatch(logout()); + }, ); \ No newline at end of file diff --git a/Apps/Client/src/actions/QuizActions.ts b/Apps/Client/src/actions/QuizActions.ts index 08d3bfb..569b9c2 100644 --- a/Apps/Client/src/actions/QuizActions.ts +++ b/Apps/Client/src/actions/QuizActions.ts @@ -1,214 +1,50 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { StatusData, GroupedScoreData } from '../types/DataTypes'; -import { CallGetQuestions } from '../calls/quiz/CallGetQuestions'; -import { CallGetStatus } from '../calls/quiz/CallGetStatus'; -import { CallGetScores } from '../calls/quiz/CallGetScores'; -import { CallGetVotes } from '../calls/quiz/CallGetVotes'; -import { QuizJSON } from '../types/JSONTypes'; -import { RootState } from '../stores/store'; import { CallStartQuiz } from '../calls/quiz/CallStartQuiz'; import { CallStartQuestion } from '../calls/quiz/CallStartQuestion'; -import { Language } from '../constants'; import { CallDeleteQuiz } from '../calls/quiz/CallDeleteQuiz'; -import { logout } from './UserActions'; +import { logout } from './AuthActions'; +import { ThunkAPI, createServerAction } from './ServerActions'; +import { CallVote } from '../calls/quiz/CallVote'; +import { VotesData } from '../types/DataTypes'; -export const fetchQuestions = createAsyncThunk( - 'quiz/fetchQuestions', - async (lang: Language, { rejectWithValue }) => { - try { - const { data } = await new CallGetQuestions(lang).execute(); - - return data as QuizJSON; - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - console.error(`Could not fetch questions: ${error}`); - return rejectWithValue(error); - } - } +type StartQuizActionArgs = { quizId: string, isSupervised: boolean }; +export const startQuiz = createServerAction( + 'quiz/start', + async ({ quizId, isSupervised }: StartQuizActionArgs) => { + await new CallStartQuiz(quizId).execute({ isSupervised }); + }, ); -export const fetchStatus = createAsyncThunk( - 'quiz/fetchStatus', - async (quizId: string, { rejectWithValue }) => { - try { - const { data } = await new CallGetStatus(quizId).execute(); - - return data as StatusData; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } +export const deleteQuiz = createServerAction( + 'quiz/delete', + async (quizId: string, { dispatch }: ThunkAPI) => { + await new CallDeleteQuiz(quizId).execute(); - console.error(`Could not fetch quiz status: ${error}`); - return rejectWithValue(error); - } - } + dispatch(logout()); + }, ); -export const fetchVotes = createAsyncThunk( - 'quiz/fetchVotes', - async (quizId: string, { rejectWithValue }) => { - try { - const { data } = await new CallGetVotes(quizId).execute(); - - return data as number[]; +type StartQuestionActionArgs = { quizId: string, questionIndex: number }; +export const startQuestion = createServerAction( + 'question/start', + async ({ quizId, questionIndex }: StartQuestionActionArgs) => { + await new CallStartQuestion(quizId, questionIndex).execute(); - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`Could not fetch user's votes: ${error}`); - return rejectWithValue(error); - } - } + return questionIndex; + }, ); -export const fetchScores = createAsyncThunk( - 'quiz/fetchScores', - async (quizId: string, { rejectWithValue }) => { - try { - const { data } = await new CallGetScores(quizId).execute(); - - return data as GroupedScoreData; +type VoteActionArgs = { quizId: string, questionIndex: number, vote: number }; +export const vote = createServerAction( + 'quiz/vote', + async ({ quizId, questionIndex, vote }: VoteActionArgs) => { + const { data } = await new CallVote(quizId, questionIndex).execute({ vote }); - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`Could not fetch scores: ${error}`); - return rejectWithValue(error); + if (!data) { + throw new Error('MISSING_DATA'); } - } -); - - - -export const fetchData = createAsyncThunk( - 'quiz/fetchData', - async ({ quizId, lang }: { quizId: string, lang: Language } , { dispatch, getState, rejectWithValue }) => { - try { - const result = await Promise.all([ - dispatch(fetchQuestions(lang)), - dispatch(fetchVotes(quizId)), - dispatch(fetchScores(quizId)), - dispatch(fetchStatus(quizId)), - ]); - - const someFetchActionFailed = result - .map(({ type }) => type) - .some(type => type.endsWith('/rejected')); - - if (someFetchActionFailed) { - throw new Error('DATA_FETCH'); - } - - const { quiz } = getState() as RootState; - const status = quiz.status.data as StatusData; - const votes = quiz.votes.data as number[]; - - // The current question index in the app corresponds to the first question - // the user hasn't answered yet, unless the player has answered all the - // questions already - const questionIndex = status.questionIndex; - const playerQuestionIndex = votes.length; - - if (playerQuestionIndex < questionIndex) { - return playerQuestionIndex; - } - return questionIndex; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`Could not fetch initial data: ${error}`); - return rejectWithValue(error); - } - } -); - -export const startQuiz = createAsyncThunk( - 'quiz/startQuiz', - async ({ quizId, isSupervised }: { quizId: string, isSupervised: boolean }, { rejectWithValue }) => { - try { - await new CallStartQuiz(quizId).execute({ isSupervised }); - - return; - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(error); - - return rejectWithValue(error); - } - } -); - -export const deleteQuiz = createAsyncThunk( - 'quiz/deleteQuiz', - async (quizId: string, { dispatch, rejectWithValue }) => { - try { - await new CallDeleteQuiz(quizId).execute(); - - dispatch(logout()); - - return; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(error); - - return rejectWithValue(error); - } - } -); - -export const startQuestion = createAsyncThunk( - 'quiz/question/start', - async ({ quizId, questionIndex }: { quizId: string, questionIndex: number }, { rejectWithValue }) => { - try { - await new CallStartQuestion(quizId, questionIndex).execute(); - - return questionIndex; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(error); - - return rejectWithValue(error); - } - } + return data as VotesData; + }, ); \ No newline at end of file diff --git a/Apps/Client/src/actions/ServerActions.ts b/Apps/Client/src/actions/ServerActions.ts new file mode 100644 index 0000000..66182b3 --- /dev/null +++ b/Apps/Client/src/actions/ServerActions.ts @@ -0,0 +1,26 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +export type ThunkAPI = { + dispatch: Function, + getState: Function, + rejectWithValue: Function +}; + +export const createServerAction = (name: string, action: (args: ActionArgs, thunkAPI: ThunkAPI) => Promise) => createAsyncThunk( + name, + async (args: ActionArgs, thunkAPI: ThunkAPI) => { + try { + return await action(args, thunkAPI); + + } catch (err: unknown) { + let error = 'UNKNOWN_ERROR'; + + if (err instanceof Error) { + error = err.message; + } + + console.error(`Could not execute fetch action '${name}': ${error}`); + return thunkAPI.rejectWithValue(error); + } + } +); \ No newline at end of file diff --git a/Apps/Client/src/actions/UserActions.ts b/Apps/Client/src/actions/UserActions.ts deleted file mode 100644 index f1c44ce..0000000 --- a/Apps/Client/src/actions/UserActions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { CallLogIn } from '../calls/auth/CallLogIn'; -import { LoginData, PingData, UserData, VotesData } from '../types/DataTypes'; -import { CallPing } from '../calls/auth/CallPing'; -import { CallLogOut } from '../calls/auth/CallLogOut'; -import { CallVote } from '../calls/quiz/CallVote'; - -export const login = createAsyncThunk( - 'user/login', - async ({ quizId, username, password }: LoginData, { rejectWithValue }) => { - try { - const { data } = await new CallLogIn().execute({ quizId, username, password }); - - if (!data) { - throw new Error('MISSING_DATA'); - } - - const user = data as UserData; - - return { - username: user.username, - isAdmin: user.isAdmin, - quizId, - }; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`Could not log user in: ${error}`); - return rejectWithValue(error); - } - } -); - -export const logout = createAsyncThunk( - 'user/logout', - async (_, { rejectWithValue }) => { - try { - await new CallLogOut().execute(); - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`Could not log user out: ${error}`); - return rejectWithValue(error); - } - } -); - -export const ping = createAsyncThunk( - 'user/ping', - async (_, { rejectWithValue }) => { - try { - const { data } = await new CallPing().execute(); - - if (!data) { - throw new Error('MISSING_DATA'); - } - - return data as PingData; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(`User is not authenticated yet: ${error}`); - return rejectWithValue(error); - } - } -); - -export const vote = createAsyncThunk( - 'user/vote', - async ({ quizId, questionIndex, vote }: { quizId: string, questionIndex: number, vote: number }, { rejectWithValue }) => { - try { - const { data } = await new CallVote(quizId, questionIndex).execute({ vote }); - - if (!data) { - throw new Error('MISSING_DATA'); - } - - return data as VotesData; - - } catch (err: unknown) { - let error = 'UNKNOWN_ERROR'; - - if (err instanceof Error) { - error = err.message; - } - - console.error(error); - - return rejectWithValue(error); - } - } -); \ No newline at end of file diff --git a/Apps/Client/src/calls/data/CallGetBackgroundUrl.ts b/Apps/Client/src/calls/data/CallGetBackgroundUrl.ts new file mode 100644 index 0000000..9733ae7 --- /dev/null +++ b/Apps/Client/src/calls/data/CallGetBackgroundUrl.ts @@ -0,0 +1,9 @@ +import { QuizName } from '../../constants'; +import CallGET from '../base/CallGET'; + +export class CallGetBackgroundUrl extends CallGET { + + constructor(quizName: QuizName) { + super(`/bg/${quizName}`); + } +}; \ No newline at end of file diff --git a/Apps/Client/src/calls/data/CallGetQuizNames.ts b/Apps/Client/src/calls/data/CallGetQuizNames.ts new file mode 100644 index 0000000..df2f78c --- /dev/null +++ b/Apps/Client/src/calls/data/CallGetQuizNames.ts @@ -0,0 +1,8 @@ +import CallGET from '../base/CallGET'; + +export class CallGetQuizNames extends CallGET { + + constructor() { + super(`/quiz`); + } +}; \ No newline at end of file diff --git a/Apps/Client/src/calls/quiz/CallGetQuestions.ts b/Apps/Client/src/calls/quiz/CallGetQuestions.ts index b41cd58..82b2e41 100644 --- a/Apps/Client/src/calls/quiz/CallGetQuestions.ts +++ b/Apps/Client/src/calls/quiz/CallGetQuestions.ts @@ -1,10 +1,10 @@ -import { Language } from '../../constants'; +import { Language, QuizName } from '../../constants'; import { QuizJSON } from '../../types/JSONTypes'; import CallGET from '../base/CallGET'; export class CallGetQuestions extends CallGET { - constructor(lang: Language) { - super(`/questions/${lang}`); + constructor(lang: Language, quizName: QuizName) { + super(`/questions/${lang}/${quizName}`); } }; \ No newline at end of file diff --git a/Apps/Client/src/calls/quiz/CallStartQuestion.ts b/Apps/Client/src/calls/quiz/CallStartQuestion.ts index b304fa0..77eb8e9 100644 --- a/Apps/Client/src/calls/quiz/CallStartQuestion.ts +++ b/Apps/Client/src/calls/quiz/CallStartQuestion.ts @@ -3,6 +3,6 @@ import CallPUT from '../base/CallPUT'; export class CallStartQuestion extends CallPUT { constructor(quizId: string, questionIndex: number) { - super(`/quiz/${quizId}/question/${questionIndex}/start`); + super(`/quiz/${quizId}/question/${questionIndex}`); } }; \ No newline at end of file diff --git a/Apps/Client/src/calls/quiz/CallStartQuiz.ts b/Apps/Client/src/calls/quiz/CallStartQuiz.ts index c3e667e..a4863db 100644 --- a/Apps/Client/src/calls/quiz/CallStartQuiz.ts +++ b/Apps/Client/src/calls/quiz/CallStartQuiz.ts @@ -7,6 +7,6 @@ type RequestData = { export class CallStartQuiz extends CallPUT { constructor(quizId: string) { - super(`/quiz/${quizId}/start`); + super(`/quiz/${quizId}`); } }; \ No newline at end of file diff --git a/Apps/Client/src/components/Nav.tsx b/Apps/Client/src/components/Nav.tsx index dd0ae18..fd5ebed 100644 --- a/Apps/Client/src/components/Nav.tsx +++ b/Apps/Client/src/components/Nav.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import './Nav.scss'; import { useDispatch, useSelector } from '../hooks/redux'; import { Link, useLocation } from 'react-router-dom'; -import { logout } from '../actions/UserActions'; +import { logout } from '../actions/AuthActions'; import OpenIcon from '@mui/icons-material/Menu'; import CloseIcon from '@mui/icons-material/Close'; import LogoutIcon from '@mui/icons-material/Logout'; @@ -106,7 +106,7 @@ const Nav: React.FC = () => {

- {isAuthenticated ? `${t('COMMON.WELCOME')}, ${username}!` : `${t('COMMON.WELCOME')}!`} + {isAuthenticated ? `${t('common:COMMON.WELCOME')}, ${username}!` : `${t('common:COMMON.WELCOME')}!`}

@@ -131,7 +131,7 @@ const Nav: React.FC = () => {
  • - {t('COMMON.QUIZ')} + {t('common:COMMON.QUIZ')}
  • )} @@ -140,7 +140,7 @@ const Nav: React.FC = () => {
  • - {t('COMMON.SCOREBOARD')} + {t('common:COMMON.SCOREBOARD')}
  • )} @@ -149,7 +149,7 @@ const Nav: React.FC = () => {
  • - {t('COMMON.TEST')} + {t('common:COMMON.TEST')}
  • )} @@ -158,7 +158,7 @@ const Nav: React.FC = () => {
  • - {t('COMMON.START_PAGE')} + {t('common:COMMON.HOME')}
  • )} @@ -167,7 +167,7 @@ const Nav: React.FC = () => {
  • - {t('COMMON.LOG_OUT')} + {t('common:COMMON.LOG_OUT')}
  • )} diff --git a/Apps/Client/src/components/PlaceholderVideo.tsx b/Apps/Client/src/components/PlaceholderVideo.tsx index a22bea3..20df951 100644 --- a/Apps/Client/src/components/PlaceholderVideo.tsx +++ b/Apps/Client/src/components/PlaceholderVideo.tsx @@ -34,7 +34,7 @@ const PlaceholderVideo: React.FC = ({ className, src, alt }) => { onLoadedMetadata={showVideo} > - {t('ERRORS.NO_VIDEO_TAGS')} + {t('common:ERRORS.NO_VIDEO_TAGS')}
    ); diff --git a/Apps/Client/src/components/Scoreboard.tsx b/Apps/Client/src/components/Scoreboard.tsx index ada4ade..9a7580f 100644 --- a/Apps/Client/src/components/Scoreboard.tsx +++ b/Apps/Client/src/components/Scoreboard.tsx @@ -29,9 +29,9 @@ const Scoreboard: React.FC = (props) => { return (
    -

    {t('COMMON.SCOREBOARD')}

    +

    {t('common:COMMON.SCOREBOARD')}

    - {t('COMMON.QUIZ')}: + {t('common:COMMON.QUIZ')}: {quizId} @@ -42,9 +42,9 @@ const Scoreboard: React.FC = (props) => { - - - + + + diff --git a/Apps/Client/src/components/forms/AdminQuizForm.tsx b/Apps/Client/src/components/forms/AdminQuizForm.tsx index 3ce42ed..8bf6456 100644 --- a/Apps/Client/src/components/forms/AdminQuizForm.tsx +++ b/Apps/Client/src/components/forms/AdminQuizForm.tsx @@ -3,9 +3,9 @@ import { useDispatch, useSelector } from '../../hooks/redux'; import './AdminQuizForm.scss'; import { deleteQuiz, startQuiz } from '../../actions/QuizActions'; import { Trans, useTranslation } from 'react-i18next'; -import { selectPlayers } from '../../reducers/QuizReducer'; import { deleteDatabase } from '../../actions/DatabaseActions'; -import { logout } from '../../actions/UserActions'; +import { logout } from '../../actions/AuthActions'; +import { selectPlayers } from '../../selectors/QuizSelectors'; const AdminQuizForm: React.FC = () => { const dispatch = useDispatch(); @@ -55,7 +55,7 @@ const AdminQuizForm: React.FC = () => { return ( -

    {t('FORMS.START_QUIZ.TITLE')}

    +

    {t('common:FORMS.START_QUIZ.TITLE')}

    { checked={isSupervised} onChange={handleChange} /> - + -

    {t('FORMS.START_QUIZ.TEXT')}

    +

    {t('common:FORMS.START_QUIZ.TEXT')}

    ); diff --git a/Apps/Client/src/components/forms/LoginForm.tsx b/Apps/Client/src/components/forms/LoginForm.tsx index 88134d5..162c2de 100644 --- a/Apps/Client/src/components/forms/LoginForm.tsx +++ b/Apps/Client/src/components/forms/LoginForm.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDispatch, useSelector } from '../../hooks/redux'; -import { selectAuthentication } from '../../reducers/UserReducer'; import './LoginForm.scss'; -import { login } from '../../actions/UserActions'; +import { login } from '../../actions/AuthActions'; import { useTranslation } from 'react-i18next'; +import { selectAuthentication } from '../../selectors/UserSelectors'; type Props = { quizId: string | null, @@ -13,8 +13,11 @@ type Props = { const LoginForm: React.FC = (props) => { const dispatch = useDispatch(); const navigate = useNavigate(); + const { t } = useTranslation(); + const quiz = useSelector((state) => state.quiz); + const [quizId, setQuizId] = useState(props.quizId ?? ''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -40,7 +43,11 @@ const LoginForm: React.FC = (props) => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await dispatch(login({ quizId, username, password })); + if (quiz.name === null) { + return; + } + + await dispatch(login({ quizId, quizName: quiz.name, username, password })); }; return ( @@ -49,7 +56,7 @@ const LoginForm: React.FC = (props) => { id='login-quiz-id' type='text' value={quizId} - placeholder={t('FORMS.LOGIN.QUIZ_ID')} + placeholder={t('common:FORMS.LOGIN.QUIZ_ID')} onChange={(e) => setQuizId(e.target.value)} required /> @@ -58,7 +65,7 @@ const LoginForm: React.FC = (props) => { id='login-username' type='text' value={username} - placeholder={t('FORMS.LOGIN.USERNAME')} + placeholder={t('common:FORMS.LOGIN.USERNAME')} onChange={(e) => setUsername(e.target.value)} required /> @@ -67,7 +74,7 @@ const LoginForm: React.FC = (props) => { id='login-password' type='password' value={password} - placeholder={t('FORMS.LOGIN.PASSWORD')} + placeholder={t('common:FORMS.LOGIN.PASSWORD')} onChange={(e) => setPassword(e.target.value)} required /> @@ -75,7 +82,7 @@ const LoginForm: React.FC = (props) => { {error &&

    {t(`ERRORS.${error}`)}

    } ); diff --git a/Apps/Client/src/components/forms/QuestionForm.tsx b/Apps/Client/src/components/forms/QuestionForm.tsx index 9077bd3..e731593 100644 --- a/Apps/Client/src/components/forms/QuestionForm.tsx +++ b/Apps/Client/src/components/forms/QuestionForm.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from '../../hooks/redux'; import { openAnswerOverlay } from '../../reducers/OverlaysReducer'; import './QuestionForm.scss'; -import { vote } from '../../actions/UserActions'; +import { vote } from '../../actions/QuizActions'; import { useTranslation } from 'react-i18next'; import { SERVER_ROOT } from '../../config'; import { AspectRatio } from '../../constants'; @@ -36,6 +36,7 @@ const QuestionForm: React.FC = (props) => { const { index, theme, question, image, video, ratio, options, disabled, choice, setChoice } = props; const { t } = useTranslation(); + const quiz = useSelector(({ quiz }) => quiz); const quizId = quiz.id; const questions = quiz.questions.data; @@ -82,7 +83,7 @@ const QuestionForm: React.FC = (props) => { return (
    -

    {t('COMMON.QUESTION')}: {index + 1}/{questions.length}

    +

    {t('common:COMMON.QUESTION')}: {index + 1}/{questions.length}

    {theme}

    diff --git a/Apps/Client/src/components/overlays/AnswerOverlay.tsx b/Apps/Client/src/components/overlays/AnswerOverlay.tsx index 5330f0f..7fb985e 100644 --- a/Apps/Client/src/components/overlays/AnswerOverlay.tsx +++ b/Apps/Client/src/components/overlays/AnswerOverlay.tsx @@ -1,6 +1,5 @@ import './AnswerOverlay.scss'; import { useDispatch, useSelector } from '../../hooks/redux'; -import { selectAnswer, selectPlayers, selectCorrectAnswer } from '../../reducers/QuizReducer'; import { closeAnswerOverlay } from '../../reducers/OverlaysReducer'; import { setQuestionIndex } from '../../reducers/AppReducer'; import { useNavigate } from 'react-router-dom'; @@ -9,6 +8,7 @@ import WrongIcon from '@mui/icons-material/Close'; import WaitIcon from '@mui/icons-material/Schedule'; import { startQuestion } from '../../actions/QuizActions'; import { useTranslation } from 'react-i18next'; +import { selectPlayers, selectAnswer, selectCorrectAnswer } from '../../selectors/QuizSelectors'; const AnswerOverlay: React.FC = () => { const dispatch = useDispatch(); @@ -48,7 +48,7 @@ const AnswerOverlay: React.FC = () => { const Icon = isAnswerCorrect ? RightIcon : WrongIcon; const iconText = t(isAnswerCorrect ? 'OVERLAYS.ANSWER.RIGHT_ANSWER_ICON_TEXT' : 'OVERLAYS.ANSWER.WRONG_ANSWER_ICON_TEXT'); - const title = t('OVERLAYS.ANSWER.CURRENT_STATUS', { voteCount, playersCount: players.length }); + const title = t('common:OVERLAYS.ANSWER.CURRENT_STATUS', { voteCount, playersCount: players.length }); const text = t(isAnswerCorrect ? 'OVERLAYS.ANSWER.RIGHT_ANSWER_TEXT' : 'OVERLAYS.ANSWER.WRONG_ANSWER_TEXT'); @@ -82,7 +82,7 @@ const AnswerOverlay: React.FC = () => { {mustWait ? ( <> -

    {t('OVERLAYS.ANSWER.PLEASE_WAIT_FOR_NEXT_QUESTION')}

    +

    {t('common:OVERLAYS.ANSWER.PLEASE_WAIT_FOR_NEXT_QUESTION')}

    ) : ( <> @@ -103,17 +103,17 @@ const AnswerOverlay: React.FC = () => { {!isOver && (isAdmin && isSupervised) && ( )} {!isOver && !(isAdmin && isSupervised) && !mustWait && ( )} {isOver && ( )} diff --git a/Apps/Client/src/components/overlays/LoadingOverlay.tsx b/Apps/Client/src/components/overlays/LoadingOverlay.tsx index 50267bf..e03a860 100644 --- a/Apps/Client/src/components/overlays/LoadingOverlay.tsx +++ b/Apps/Client/src/components/overlays/LoadingOverlay.tsx @@ -1,7 +1,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from '../../hooks/redux'; import './LoadingOverlay.scss'; -import { selectPlayers } from '../../reducers/QuizReducer'; +import { selectPlayers } from '../../selectors/QuizSelectors'; const LoadingOverlay: React.FC = () => { const { t } = useTranslation(); @@ -19,7 +19,7 @@ const LoadingOverlay: React.FC = () => {

    - {t('COMMON.PLEASE_WAIT')}... + {t('common:COMMON.PLEASE_WAIT')}...

    diff --git a/Apps/Client/src/config/index.ts b/Apps/Client/src/config/index.ts index 3a473fa..88e1038 100644 --- a/Apps/Client/src/config/index.ts +++ b/Apps/Client/src/config/index.ts @@ -6,12 +6,4 @@ export const DEBUG = ENV === Environment.Development; export const SERVER_ROOT = DEBUG ? `http://localhost:8000` : ``; export const API_ROOT = `${SERVER_ROOT}/api/v1`; -export const REFRESH_STATUS_INTERVAL = 5_000; // (ms) - -export const BACKGROUND_URLS = [ - `background-1.jpg`, - `background-2.jpg`, - `background-3.jpg`, - `background-4.webp`, -] -.map(filename => `${SERVER_ROOT}/static/img/bg/liquors/${filename}`); \ No newline at end of file +export const REFRESH_STATUS_INTERVAL = 5_000; // (ms) \ No newline at end of file diff --git a/Apps/Client/src/constants/index.ts b/Apps/Client/src/constants/index.ts index f2e6fb3..e541280 100644 --- a/Apps/Client/src/constants/index.ts +++ b/Apps/Client/src/constants/index.ts @@ -9,6 +9,11 @@ export enum Language { DE = 'de', }; +export enum QuizName { + Liquors = 'liquors', + KonnyUndJohannes = 'k-und-j', +} + export enum QuestionType { Text = 'text', Image = 'image', diff --git a/Apps/Client/src/i18n.ts b/Apps/Client/src/i18n.ts index ef06ef7..b1f9b3a 100644 --- a/Apps/Client/src/i18n.ts +++ b/Apps/Client/src/i18n.ts @@ -1,32 +1,27 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import translationEN from './locales/en/translation.json'; -import translationDE from './locales/de/translation.json'; -import { DEBUG } from './config'; -import { Language } from './constants'; - -const resources = { - en: { - translation: translationEN, - }, - de: { - translation: translationDE, - }, -}; +import HttpBackend from 'i18next-http-backend'; +import { DEBUG, SERVER_ROOT } from './config'; +import { Language, QuizName } from './constants'; export const INIT_LANGUAGE = Language.DE; -export const FALLBACK_LANGUAGE = Language.EN; +export const FALLBACK_LANGUAGE = Language.DE; i18n + .use(HttpBackend) .use(initReactI18next) .init({ debug: DEBUG, - resources, lng: INIT_LANGUAGE, fallbackLng: FALLBACK_LANGUAGE, + ns: ['common', QuizName.Liquors, QuizName.KonnyUndJohannes], + defaultNS: 'common', interpolation: { escapeValue: false, }, + backend: { + loadPath: `${SERVER_ROOT}/static/locales/{{lng}}/{{ns}}.json`, + }, }); export default i18n; \ No newline at end of file diff --git a/Apps/Client/src/index.tsx b/Apps/Client/src/index.tsx index 588c46e..9a89d38 100644 --- a/Apps/Client/src/index.tsx +++ b/Apps/Client/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; @@ -21,7 +21,9 @@ root.render( - + + + diff --git a/Apps/Client/src/pages/ErrorPage.tsx b/Apps/Client/src/pages/ErrorPage.tsx index bf517c8..ac3aea6 100644 --- a/Apps/Client/src/pages/ErrorPage.tsx +++ b/Apps/Client/src/pages/ErrorPage.tsx @@ -37,10 +37,10 @@ const ErrorPage: React.FC = () => { }, []); return ( - +

    -

    {t('PAGES.ERROR.TITLE')}

    -

    {t('PAGES.ERROR.TEXT')}

    +

    {t('common:PAGES.ERROR.TITLE')}

    +

    {t('common:PAGES.ERROR.TEXT')}

    {remainingTime === 1 ? ( diff --git a/Apps/Client/src/pages/HomePage.tsx b/Apps/Client/src/pages/HomePage.tsx index 74ee394..1caa185 100644 --- a/Apps/Client/src/pages/HomePage.tsx +++ b/Apps/Client/src/pages/HomePage.tsx @@ -1,32 +1,69 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import './HomePage.scss'; import LoginForm from '../components/forms/LoginForm'; -import { useSelector } from '../hooks/redux'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useDispatch, useSelector } from '../hooks/redux'; +import { Navigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Page from './Page'; +import { QuizName } from '../constants'; +import { setQuizName } from '../reducers/QuizReducer'; + +const QUIZ_ID_PARAM = 'id'; +const QUIZ_NAME_PARAM = 'q'; + + const HomePage: React.FC = () => { - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); + const { t } = useTranslation(); - const navigate = useNavigate(); + const dispatch = useDispatch(); + const quiz = useSelector(({ quiz }) => quiz); + const data = useSelector(({ data }) => data); const isAuthenticated = useSelector((state) => state.user.isAuthenticated); - if (isAuthenticated) { - navigate('/quiz'); + const paramQuizId = searchParams.get(QUIZ_ID_PARAM); + const paramQuizName = searchParams.get(QUIZ_NAME_PARAM); + + const quizId = paramQuizId; + const quizName = paramQuizName as QuizName ?? quiz.name; + const isQuizNameValid = data.quizzes.includes(quizName); + + // Store quiz name in app state when valid + useEffect(() => { + if (isQuizNameValid) { + dispatch(setQuizName(quizName)); + } + }, [isQuizNameValid]); + + // Clean up URL from quiz name + useEffect(() => { + if (searchParams.has(QUIZ_NAME_PARAM)) { + searchParams.delete(QUIZ_NAME_PARAM); + + setSearchParams(searchParams); + } + }, [searchParams, setSearchParams]); + + if (!isQuizNameValid) { + return null; } - const quizId = searchParams.get('id'); + if (isAuthenticated) { + return ( + + ); + } return ( - + {!isAuthenticated && (

    -

    {t('PAGES.HOME.TITLE')}

    -

    {t('PAGES.HOME.WELCOME_HEAD')}

    -

    {t('PAGES.HOME.WELCOME_TEXT')}

    -

    {t('PAGES.HOME.WELCOME_CTA')}

    +

    {t(`${quizName}:TITLE`)}

    +

    {t(`${quizName}:WELCOME_HEAD`)}

    +

    {t(`${quizName}:WELCOME_TEXT`)}

    +

    {t(`${quizName}:WELCOME_CTA`)}

    diff --git a/Apps/Client/src/pages/Page.tsx b/Apps/Client/src/pages/Page.tsx index 6613280..9bda00a 100644 --- a/Apps/Client/src/pages/Page.tsx +++ b/Apps/Client/src/pages/Page.tsx @@ -1,17 +1,23 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import './Page.scss'; import { useSelector } from '../hooks/redux'; type Props = { children: React.ReactNode, className: string, + title: string, } const Page: React.FC = (props) => { - const { children, className } = props; + const { children, className, title } = props; const app = useSelector((state) => state.app); + // Set page title + useEffect(() => { + document.title = title; + }, [title]); + return (
    {children} diff --git a/Apps/Client/src/pages/QuizPage.tsx b/Apps/Client/src/pages/QuizPage.tsx index d19de23..93a25e2 100644 --- a/Apps/Client/src/pages/QuizPage.tsx +++ b/Apps/Client/src/pages/QuizPage.tsx @@ -3,17 +3,18 @@ import './QuizPage.scss'; import QuestionForm from '../components/forms/QuestionForm'; import { useDispatch, useSelector } from '../hooks/redux'; import { REFRESH_STATUS_INTERVAL } from '../config'; -import { fetchStatus, fetchData, fetchQuestions } from '../actions/QuizActions'; -import { selectVote } from '../reducers/QuizReducer'; +import { fetchStatus, fetchQuizData, fetchQuestions } from '../actions/DataActions'; import { closeAnswerOverlay, closeLoadingOverlay, openAnswerOverlay, openLoadingOverlay } from '../reducers/OverlaysReducer'; import AdminQuizForm from '../components/forms/AdminQuizForm'; import { useTranslation } from 'react-i18next'; import { AspectRatio, Language, QuestionType } from '../constants'; -import { logout } from '../actions/UserActions'; +import { logout } from '../actions/AuthActions'; import Page from './Page'; +import { selectVote } from '../selectors/QuizSelectors'; + +const QuizPage: React.FC = () => { + const { t, i18n } = useTranslation(); -const QuizPage: React.FC = () => { - const { i18n } = useTranslation(); const lang = i18n.language as Language; const quiz = useSelector(({ quiz }) => quiz); @@ -27,6 +28,7 @@ const QuizPage: React.FC = () => { const dispatch = useDispatch(); const quizId = quiz.id; + const quizName = quiz.name; const questions = quiz.questions.data; const status = quiz.status.data; const isStarted = status?.isStarted; @@ -35,25 +37,25 @@ const QuizPage: React.FC = () => { // Fetch initial data useEffect(() => { - if (quizId === null) { + if (quizId === null || quizName === null) { return; } - dispatch(fetchData({ quizId, lang })); + dispatch(fetchQuizData({ quizId, quizName, lang })); }, []); // Refresh quiz data when changing language useEffect(() => { - if (quizId === null) { + if (quizId === null || quizName === null) { return; } - dispatch(fetchQuestions(lang)); + dispatch(fetchQuestions({ lang, quizName })); }, [lang]); // Regularly fetch current quiz status from server useEffect(() => { - if (quizId === null) { + if (quizId === null || quizName === null) { return; } @@ -101,7 +103,7 @@ const QuizPage: React.FC = () => { const { theme, question, type, url, options } = questions[playerQuestionIndex]; return ( - + {!isStarted && isAdmin && ( )} diff --git a/Apps/Client/src/pages/ScoresPage.tsx b/Apps/Client/src/pages/ScoresPage.tsx index 03cc8c5..93288d1 100644 --- a/Apps/Client/src/pages/ScoresPage.tsx +++ b/Apps/Client/src/pages/ScoresPage.tsx @@ -2,14 +2,16 @@ import React, { useEffect } from 'react'; import './ScoresPage.scss'; import Scoreboard from '../components/Scoreboard'; import { useDispatch, useSelector } from '../hooks/redux'; -import { fetchScores } from '../actions/QuizActions'; -import { useNavigate } from 'react-router-dom'; +import { fetchScores } from '../actions/DataActions'; +import { Navigate } from 'react-router-dom'; import { closeAllOverlays } from '../reducers/OverlaysReducer'; import Page from './Page'; +import { useTranslation } from 'react-i18next'; const ScoresPage: React.FC = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); - const navigate = useNavigate(); const quiz = useSelector(({ quiz }) => quiz); const quizId = quiz.id; @@ -35,12 +37,15 @@ const ScoresPage: React.FC = () => { } const isStarted = status.isStarted; + if (!isStarted) { - navigate('/quiz'); + return ( + + ); } return ( - + ); diff --git a/Apps/Client/src/pages/TestPage.tsx b/Apps/Client/src/pages/TestPage.tsx index 9d68ccd..0e6ce83 100644 --- a/Apps/Client/src/pages/TestPage.tsx +++ b/Apps/Client/src/pages/TestPage.tsx @@ -10,7 +10,7 @@ const TestPage: React.FC = () => { dispatch(closeAllOverlays()); return ( - +

    Test page

    This is the test page.

    diff --git a/Apps/Client/src/reducers/AppReducer.ts b/Apps/Client/src/reducers/AppReducer.ts index 7582848..8581573 100644 --- a/Apps/Client/src/reducers/AppReducer.ts +++ b/Apps/Client/src/reducers/AppReducer.ts @@ -1,8 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { logout } from '../actions/UserActions'; -import { fetchData, startQuestion } from '../actions/QuizActions'; +import { logout } from '../actions/AuthActions'; +import { startQuestion } from '../actions/QuizActions'; import { Language } from '../constants'; import { INIT_LANGUAGE } from '../i18n'; +import { fetchQuizData } from '../actions/DataActions'; interface AppState { language: Language, @@ -45,7 +46,7 @@ export const appSlice = createSlice({ language: state.language, version: state.version, })) - .addCase(fetchData.fulfilled, (state, action) => { + .addCase(fetchQuizData.fulfilled, (state, action) => { state.questionIndex = action.payload as number; }) .addCase(startQuestion.fulfilled, (state, action) => { diff --git a/Apps/Client/src/reducers/DataReducer.ts b/Apps/Client/src/reducers/DataReducer.ts new file mode 100644 index 0000000..666b721 --- /dev/null +++ b/Apps/Client/src/reducers/DataReducer.ts @@ -0,0 +1,26 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { fetchQuizNames } from '../actions/DataActions'; + +interface DataState { + quizzes: string[], +} + +const initialState: DataState = { + quizzes: [], +}; + + + +export const dataSlice = createSlice({ + name: 'data', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchQuizNames.fulfilled, (state, action) => { + state.quizzes = action.payload as string[]; + }); + }, +}); + +export default dataSlice.reducer; \ No newline at end of file diff --git a/Apps/Client/src/reducers/OverlaysReducer.ts b/Apps/Client/src/reducers/OverlaysReducer.ts index 3146848..86b9fb7 100644 --- a/Apps/Client/src/reducers/OverlaysReducer.ts +++ b/Apps/Client/src/reducers/OverlaysReducer.ts @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { logout } from '../actions/UserActions'; +import { logout } from '../actions/AuthActions'; interface OverlaysState { loading: { diff --git a/Apps/Client/src/reducers/QuizReducer.ts b/Apps/Client/src/reducers/QuizReducer.ts index a343fd1..3ea62e1 100644 --- a/Apps/Client/src/reducers/QuizReducer.ts +++ b/Apps/Client/src/reducers/QuizReducer.ts @@ -1,13 +1,15 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { FetchedData, GroupedScoreData, StatusData } from '../types/DataTypes'; import { getInitialFetchedData } from '../utils'; -import { fetchQuestions, fetchStatus, fetchVotes, fetchScores, startQuiz, startQuestion } from '../actions/QuizActions'; -import { login, logout, ping, vote } from '../actions/UserActions'; +import { startQuiz, startQuestion, vote } from '../actions/QuizActions'; +import { login, logout, ping } from '../actions/AuthActions'; import { QuizJSON } from '../types/JSONTypes'; -import { RootState } from '../stores/store'; +import { QuizName } from '../constants'; +import { fetchStatus, fetchQuestions, fetchVotes, fetchScores } from '../actions/DataActions'; interface QuizState { id: string | null, + name: QuizName | null, questions: FetchedData, status: FetchedData, votes: FetchedData, @@ -16,6 +18,7 @@ interface QuizState { const initialState: QuizState = { id: null, + name: null, questions: getInitialFetchedData(), status: getInitialFetchedData(), votes: getInitialFetchedData(), @@ -27,7 +30,11 @@ const initialState: QuizState = { export const quizSlice = createSlice({ name: 'quiz', initialState, - reducers: {}, + reducers: { + setQuizName: (state, action: PayloadAction) => { + state.name = action.payload; + }, + }, extraReducers: (builder) => { builder // Fetching actions @@ -117,106 +124,22 @@ export const quizSlice = createSlice({ state.id = action.payload.quizId }) // Reset state on logout, no matter if successful or not - .addCase(logout.fulfilled, () => initialState) - .addCase(logout.rejected, () => initialState); + .addCase(logout.fulfilled, (state) => { + return { + ...initialState, + name: state.name, + }; + }) + .addCase(logout.rejected, (state) => { + return { + ...initialState, + name: state.name, + }; + }); ; }, }); -// export const { } = quizSlice.actions; - -export const selectPlayers = (state: RootState) => { - const status = state.quiz.status.data; - - if (status === null) { - return []; - } - - return status.players; -} - -export const selectQuestion = (state: RootState, questionIndex: number) => { - const quiz = state.quiz; - - const questions = quiz.questions.data; - const votes = quiz.votes.data; - - if (questions === null || votes === null) { - return null; - } - - return questions[questionIndex]; -} - -export const selectAnswer = (state: RootState, questionIndex: number) => { - const quiz = state.quiz; - - const questions = quiz.questions.data; - const votes = quiz.votes.data; - - if (questions === null || votes === null) { - return null; - } - - const question = questions[questionIndex]; - const vote = votes[questionIndex]; - const answer = question.options[vote]; - - return answer; -} - -export const selectCorrectAnswer = (state: RootState, questionIndex: number) => { - const quiz = state.quiz; - - const questions = quiz.questions.data; - - if (questions === null) { - return null; - } - - const question = questions[questionIndex]; - const answer = question.options[question.answer]; - - return answer; -} - -export const selectVote = (state: RootState, questionIndex: number) => { - const quiz = state.quiz; - - const questions = quiz.questions.data; - const votes = quiz.votes.data; - - if (questions === null || votes === null || votes.length < questionIndex + 1) { - return { - voteIndex: null, - vote: null, - }; - } - - const question = questions[questionIndex]; - const voteIndex = votes[questionIndex]; - const vote = question.options[voteIndex]; - - return { - voteIndex, - vote, - }; -} - -export const haveAllPlayersAnswered = (state: RootState, questionIndex: number) => { - const quiz = state.quiz; - - const status = quiz.status.data; - const votes = quiz.votes.data; - const players = selectPlayers(state); - - if (status === null || votes === null || players === null) { - return false; - } - - const { votesCount } = status; - - return votesCount[questionIndex] === players.length; -} +export const { setQuizName } = quizSlice.actions; export default quizSlice.reducer; \ No newline at end of file diff --git a/Apps/Client/src/reducers/UserReducer.ts b/Apps/Client/src/reducers/UserReducer.ts index d4bb5f6..1c8d5f7 100644 --- a/Apps/Client/src/reducers/UserReducer.ts +++ b/Apps/Client/src/reducers/UserReducer.ts @@ -1,6 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { RootState } from '../stores/store'; -import { login, logout, ping } from '../actions/UserActions'; +import { login, logout, ping } from '../actions/AuthActions'; interface UserState { username: string | null, @@ -67,6 +66,4 @@ export const userSlice = createSlice({ }, }); -export const selectAuthentication = (state: RootState) => state.user; - export default userSlice.reducer; \ No newline at end of file diff --git a/Apps/Client/src/reducers/index.ts b/Apps/Client/src/reducers/index.ts index 9e1c9a6..0d83eee 100644 --- a/Apps/Client/src/reducers/index.ts +++ b/Apps/Client/src/reducers/index.ts @@ -3,9 +3,11 @@ import AppReducer from './AppReducer'; import UserReducer from './UserReducer'; import QuizReducer from './QuizReducer'; import OverlaysReducer from './OverlaysReducer'; +import DataReducer from './DataReducer'; const rootReducer = combineReducers({ app: AppReducer, + data: DataReducer, user: UserReducer, quiz: QuizReducer, overlays: OverlaysReducer, diff --git a/Apps/Client/src/selectors/QuizSelectors.ts b/Apps/Client/src/selectors/QuizSelectors.ts new file mode 100644 index 0000000..73facb3 --- /dev/null +++ b/Apps/Client/src/selectors/QuizSelectors.ts @@ -0,0 +1,95 @@ +import { RootState } from '../stores/store'; + +export const selectPlayers = (state: RootState) => { + const status = state.quiz.status.data; + + if (status === null) { + return []; + } + + return status.players; + } + + export const selectQuestion = (state: RootState, questionIndex: number) => { + const quiz = state.quiz; + + const questions = quiz.questions.data; + const votes = quiz.votes.data; + + if (questions === null || votes === null) { + return null; + } + + return questions[questionIndex]; + } + + export const selectAnswer = (state: RootState, questionIndex: number) => { + const quiz = state.quiz; + + const questions = quiz.questions.data; + const votes = quiz.votes.data; + + if (questions === null || votes === null) { + return null; + } + + const question = questions[questionIndex]; + const vote = votes[questionIndex]; + const answer = question.options[vote]; + + return answer; + } + + export const selectCorrectAnswer = (state: RootState, questionIndex: number) => { + const quiz = state.quiz; + + const questions = quiz.questions.data; + + if (questions === null) { + return null; + } + + const question = questions[questionIndex]; + const answer = question.options[question.answer]; + + return answer; + } + + export const selectVote = (state: RootState, questionIndex: number) => { + const quiz = state.quiz; + + const questions = quiz.questions.data; + const votes = quiz.votes.data; + + if (questions === null || votes === null || votes.length < questionIndex + 1) { + return { + voteIndex: null, + vote: null, + }; + } + + const question = questions[questionIndex]; + const voteIndex = votes[questionIndex]; + const vote = question.options[voteIndex]; + + return { + voteIndex, + vote, + }; + } + + export const haveAllPlayersAnswered = (state: RootState, questionIndex: number) => { + const quiz = state.quiz; + + const status = quiz.status.data; + const votes = quiz.votes.data; + const players = selectPlayers(state); + + if (status === null || votes === null || players === null) { + return false; + } + + const { votesCount } = status; + + return votesCount[questionIndex] === players.length; + } \ No newline at end of file diff --git a/Apps/Client/src/selectors/UserSelectors.ts b/Apps/Client/src/selectors/UserSelectors.ts new file mode 100644 index 0000000..07d90ed --- /dev/null +++ b/Apps/Client/src/selectors/UserSelectors.ts @@ -0,0 +1,3 @@ +import { RootState } from '../stores/store'; + +export const selectAuthentication = (state: RootState) => state.user; \ No newline at end of file diff --git a/Apps/Client/src/types/DataTypes.ts b/Apps/Client/src/types/DataTypes.ts index a829dff..017d27a 100644 --- a/Apps/Client/src/types/DataTypes.ts +++ b/Apps/Client/src/types/DataTypes.ts @@ -1,4 +1,5 @@ import { Auth } from '.'; +import { QuizName } from '../constants'; export type FetchedData = { data: Data | null, @@ -14,6 +15,7 @@ export type PingData = { export type LoginData = Auth & { quizId: string, + quizName: QuizName, }; export type UserData = { diff --git a/Apps/Server/.env.production b/Apps/Server/.env.production index eca3857..0fc1f63 100644 --- a/Apps/Server/.env.production +++ b/Apps/Server/.env.production @@ -3,7 +3,7 @@ LOGGING_LEVEL=trace HOST=localhost PORT=8000 -REDIS_HOST=liquors-quiz-redis +REDIS_HOST=quiz-redis REDIS_PORT=6379 CLIENT_HOST= diff --git a/Apps/Server/data/de/k-und-j.json b/Apps/Server/data/de/k-und-j.json new file mode 100644 index 0000000..7e73fa9 --- /dev/null +++ b/Apps/Server/data/de/k-und-j.json @@ -0,0 +1,25 @@ +[ + { + "theme": "Video", + "question": "Was ist in diesem Video los?", + "options": ["Ein Business-Meeting", "Eine Weinprobe", "Eine Software-Anleitung"], + "type": "video", + "url": "/static/video/Test.mp4", + "answer": 0 + }, + { + "theme": "Bild", + "question": "Wer ist auf diesem Bild zu sehen?", + "options": ["A", "B", "C"], + "type": "image", + "url": "/static/img/Kid.webp", + "answer": 2 + }, + { + "theme": "Test", + "question": "Was ist die Hauptzutat im traditionellen russischen Wodka?", + "options": ["Kartoffeln", "Trauben", "Weizen"], + "type": "text", + "answer": 2 + } +] \ No newline at end of file diff --git a/Apps/Server/data/de/liquors/quiz.json b/Apps/Server/data/de/liquors.json similarity index 100% rename from Apps/Server/data/de/liquors/quiz.json rename to Apps/Server/data/de/liquors.json diff --git a/Apps/Server/data/en/k-und-j.json b/Apps/Server/data/en/k-und-j.json new file mode 100644 index 0000000..2db6704 --- /dev/null +++ b/Apps/Server/data/en/k-und-j.json @@ -0,0 +1,25 @@ +[ + { + "theme": "Video", + "question": "What is going on in this video?", + "options": ["A business meeting", "A wine tasting", "A software demo"], + "type": "video", + "url": "/static/video/Test.mp4", + "answer": 0 + }, + { + "theme": "Image", + "question": "Who is on this picture?", + "options": ["A", "B", "C"], + "type": "image", + "url": "/static/img/Kid.webp", + "answer": 2 + }, + { + "theme": "Test", + "question": "What is the main ingredient in traditional Russian vodka?", + "options": ["Potatoes", "Grapes", "Wheat"], + "type": "text", + "answer": 2 + } +] \ No newline at end of file diff --git a/Apps/Server/data/en/liquors/quiz.json b/Apps/Server/data/en/liquors.json similarity index 100% rename from Apps/Server/data/en/liquors/quiz.json rename to Apps/Server/data/en/liquors.json diff --git a/Apps/Server/package-lock.json b/Apps/Server/package-lock.json index f60074b..0cfb03b 100644 --- a/Apps/Server/package-lock.json +++ b/Apps/Server/package-lock.json @@ -1,11 +1,11 @@ { - "name": "liquors-quiz--server", + "name": "quiz--server", "version": "latest", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "liquors-quiz--server", + "name": "quiz--server", "version": "latest", "dependencies": { "bcrypt": "^5.1.1", diff --git a/Apps/Server/package.json b/Apps/Server/package.json index 9e30ab6..3c2c7f8 100644 --- a/Apps/Server/package.json +++ b/Apps/Server/package.json @@ -1,5 +1,5 @@ { - "name": "liquors-quiz--server", + "name": "quiz--server", "label": "Server", "version": "latest", "author": "David Leclerc", diff --git a/Apps/Server/public/img/Kid.webp b/Apps/Server/public/img/Kid.webp new file mode 100644 index 0000000..a5d3400 Binary files /dev/null and b/Apps/Server/public/img/Kid.webp differ diff --git a/Apps/Server/public/img/bg/k-und-j/background-1.jpg b/Apps/Server/public/img/bg/k-und-j/background-1.jpg new file mode 100644 index 0000000..0be4d43 Binary files /dev/null and b/Apps/Server/public/img/bg/k-und-j/background-1.jpg differ diff --git a/Apps/Server/public/img/bg/k-und-j/background-2.jpg b/Apps/Server/public/img/bg/k-und-j/background-2.jpg new file mode 100644 index 0000000..48f59b8 Binary files /dev/null and b/Apps/Server/public/img/bg/k-und-j/background-2.jpg differ diff --git a/Apps/Client/src/locales/de/translation.json b/Apps/Server/public/locales/de/common.json similarity index 96% rename from Apps/Client/src/locales/de/translation.json rename to Apps/Server/public/locales/de/common.json index d423b57..3c6f02f 100644 --- a/Apps/Client/src/locales/de/translation.json +++ b/Apps/Server/public/locales/de/common.json @@ -8,6 +8,7 @@ "TEST": "Test", "START": "Start", "STOP": "Stop", + "ERROR": "Fehler", "LOG_IN": "Einloggen", "LOG_OUT": "Ausloggen", "USERNAME": "Benutzername", @@ -16,7 +17,7 @@ "SCOREBOARD": "Punktestand", "SCORE": "Punktzahl", "RANK": "Rang", - "START_PAGE": "Startseite" + "HOME": "Startseite" }, "PAGES": { "HOME": { @@ -39,8 +40,8 @@ "FORMS": { "LOGIN": { "QUIZ_ID": "Gib eine Quiz-ID ein", - "USERNAME": "Gib einen Benutzernamen ein", - "PASSWORD": "Gib ein Passwort ein", + "USERNAME": "Gib einen einzigartigen Benutzernamen ein", + "PASSWORD": "Gib ein beliebiges Passwort ein", "SUBMIT": "Los geht's!" }, "START_QUIZ": { diff --git a/Apps/Server/public/locales/de/k-und-j.json b/Apps/Server/public/locales/de/k-und-j.json new file mode 100644 index 0000000..4b53527 --- /dev/null +++ b/Apps/Server/public/locales/de/k-und-j.json @@ -0,0 +1,6 @@ +{ + "TITLE": "Konny & Johannes", + "WELCOME_HEAD": "Willkommen zum Quiz des 60. Jubiläums unserer lieben Freunde, Kornelia und Johannes.", + "WELCOME_TEXT": "Wer kennt sie am besten? Es werden Bilder und Videos angezeigt, sowie Mehrfachauswahlfragen gestellt, um dies herauszufinden.", + "WELCOME_CTA": "Bist Du bereit? Dann logge dich ein!" +} \ No newline at end of file diff --git a/Apps/Server/public/locales/de/liquors.json b/Apps/Server/public/locales/de/liquors.json new file mode 100644 index 0000000..61796be --- /dev/null +++ b/Apps/Server/public/locales/de/liquors.json @@ -0,0 +1,6 @@ +{ + "TITLE": "Spirituosen der Welt", + "WELCOME_HEAD": "Willkommen zum Quiz heute Abend!", + "WELCOME_TEXT": "Bereite dich darauf vor, dein Meisterwissen über die Spirituosen der Welt in einem epischen Quiz zu zeigen, wo nur die schlauesten Schnapskenner den Sieg erringen werden...", + "WELCOME_CTA": "Bist Du bereit?" +} \ No newline at end of file diff --git a/Apps/Client/src/locales/en/translation.json b/Apps/Server/public/locales/en/common.json similarity index 98% rename from Apps/Client/src/locales/en/translation.json rename to Apps/Server/public/locales/en/common.json index 6fcc47d..d7b359e 100644 --- a/Apps/Client/src/locales/en/translation.json +++ b/Apps/Server/public/locales/en/common.json @@ -8,6 +8,7 @@ "TEST": "Test", "START": "Start", "STOP": "Stop", + "ERROR": "Error", "LOG_IN": "Log in", "LOG_OUT": "Log out", "USERNAME": "Username", @@ -16,7 +17,7 @@ "SCOREBOARD": "Scoreboard", "SCORE": "Score", "RANK": "Rank", - "START_PAGE": "Homepage" + "HOME": "Homepage" }, "PAGES": { "HOME": { diff --git a/Apps/Server/public/locales/en/k-und-j.json b/Apps/Server/public/locales/en/k-und-j.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/Apps/Server/public/locales/en/k-und-j.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/Apps/Server/public/locales/en/liquors.json b/Apps/Server/public/locales/en/liquors.json new file mode 100644 index 0000000..65c9a58 --- /dev/null +++ b/Apps/Server/public/locales/en/liquors.json @@ -0,0 +1,6 @@ +{ + "TITLE": "Liquors of the World", + "WELCOME_HEAD": "Welcome to tonight's quiz!", + "WELCOME_TEXT": "Get ready to showcase your mastery of the world's spirits in an epic quiz, where only the savviest liquor aficionados will manage to claim victory...", + "WELCOME_CTA": "Are you ready?" +} \ No newline at end of file diff --git a/Apps/Server/src/config/index.ts b/Apps/Server/src/config/index.ts index 29b4233..bb4dfee 100644 --- a/Apps/Server/src/config/index.ts +++ b/Apps/Server/src/config/index.ts @@ -30,12 +30,12 @@ export const CLIENT_ROOT = `http://${CLIENT_HOST}:${CLIENT_PORT}`; // Redis export const REDIS_HOST = process.env.REDIS_HOST!; export const REDIS_PORT = parseNumberText(process.env.REDIS_PORT); -export const REDIS_NAME = `liquors`; +export const REDIS_NAME = `quiz`; export const REDIS_RETRY_CONNECT_MAX_DELAY = new TimeDuration(3, TimeUnit.Seconds); export const REDIS_RETRY_CONNECT_TIMEOUT = new TimeDuration(5, TimeUnit.Seconds); export const REDIS_RETRY_CONNECT_MAX = 5; // Authentication -export const COOKIE_NAME = `liquors`; +export const COOKIE_NAME = `quiz`; export const TOKEN_SECRET = process.env.TOKEN_SECRET!; export const N_SALT_ROUNDS = 10; \ No newline at end of file diff --git a/Apps/Server/src/constants/index.ts b/Apps/Server/src/constants/index.ts index 36aac19..4d7fe39 100644 --- a/Apps/Server/src/constants/index.ts +++ b/Apps/Server/src/constants/index.ts @@ -1,7 +1,5 @@ import { name, version, label } from '../../package.json'; import { Environment } from '../types'; -import questionsEN from '../../data/en/liquors/quiz.json'; -import questionsDE from '../../data/de/liquors/quiz.json'; export const ENVIRONMENTS = Object.values(Environment); @@ -10,13 +8,19 @@ export enum Language { DE = 'de', } -export const LANGUAGES = Object.values(Language); +export enum QuizName { + Liquors = 'liquors', + KonnyUndJohannes = 'k-und-j', +} -export const QUESTIONS_EN = questionsEN; -export const QUESTIONS_DE = questionsDE; -export const ANSWERS_EN = QUESTIONS_EN.map(({ answer }) => answer); -export const ANSWERS_DE = QUESTIONS_DE.map(({ answer }) => answer); -export const N_QUESTIONS = QUESTIONS_EN.length; +export enum QuestionType { + Text = 'text', + Image = 'image', + Video = 'video', +}; + +export const QUIZ_NAMES = Object.values(QuizName); +export const LANGUAGES = Object.values(Language); export const PACKAGE_NAME = name; export const PACKAGE_VERSION = version; diff --git a/Apps/Server/src/controllers/app/GetBackgroundUrlController.ts b/Apps/Server/src/controllers/app/GetBackgroundUrlController.ts new file mode 100644 index 0000000..f352f8c --- /dev/null +++ b/Apps/Server/src/controllers/app/GetBackgroundUrlController.ts @@ -0,0 +1,42 @@ +import { RequestHandler } from 'express'; +import { join } from 'path'; +import { successResponse } from '../../utils/calls'; +import { ParamsDictionary } from 'express-serve-static-core'; +import InvalidParamsError from '../../errors/InvalidParamsError'; +import { QUIZ_NAMES, QuizName } from '../../constants'; +import InvalidQuizNameError from '../../errors/InvalidQuizNameError'; +import { listFiles } from '../../utils/file'; +import { PUBLIC_DIR } from '../../config'; +import { getRandom } from '../../utils/array'; + +const validateParams = async (params: ParamsDictionary) => { + const { quizName } = params; + + if (quizName === undefined) { + throw new InvalidParamsError(); + } + + if (!QUIZ_NAMES.includes(quizName as QuizName)) { + throw new InvalidQuizNameError(); + } + + return { quizName }; +} + + + +const GetBackgroundUrlController: RequestHandler = async (req, res, next) => { + try { + const { quizName } = await validateParams(req.params); + + const filenames = await listFiles(join(__dirname, `../../../public/img/bg/${quizName}`)); + const urls = filenames.map((filename) => `/static/img/bg/${quizName}/${filename}`); + + return res.json(successResponse(getRandom(urls))); + + } catch (err: any) { + next(err); + } +} + +export default GetBackgroundUrlController; \ No newline at end of file diff --git a/Apps/Server/src/controllers/auth/LoginController.ts b/Apps/Server/src/controllers/auth/LoginController.ts index c6b6687..699b689 100644 --- a/Apps/Server/src/controllers/auth/LoginController.ts +++ b/Apps/Server/src/controllers/auth/LoginController.ts @@ -13,9 +13,11 @@ import InvalidPasswordError from '../../errors/InvalidPasswordError'; import UserDoesNotExistError from '../../errors/UserDoesNotExistError'; import QuizAlreadyStartedError from '../../errors/QuizAlreadyStartedError'; import { Quiz } from '../../types/QuizTypes'; +import { QuizName } from '../../constants'; type RequestBody = Auth & { quizId: string, + quizName: QuizName, }; const isPasswordValid = async (password: string, hashedPassword: string) => { @@ -42,10 +44,10 @@ const isPasswordValid = async (password: string, hashedPassword: string) => { const LoginController: RequestHandler = async (req, res, next) => { try { - const { quizId, username, password } = req.body as RequestBody; + const { quizId, quizName, username, password } = req.body as RequestBody; const admin = ADMINS.find(admin => admin.username === username); const isAdmin = Boolean(admin); - logger.trace(`Attempt to join quiz '${quizId}' as ${isAdmin ? 'admin' : 'user'} '${username}'...`); + logger.trace(`Attempt to join quiz '${quizName}' with ID '${quizId}' as ${isAdmin ? 'admin' : 'user'} '${username}'...`); // Check if quiz exists let quiz = await APP_DB.getQuiz(quizId); @@ -59,7 +61,7 @@ const LoginController: RequestHandler = async (req, res, next) => { } // Only admins can create new quizzes - quiz = await APP_DB.createQuiz(quizId, username); + quiz = await APP_DB.createQuiz(quizId, quizName, username); } const isQuizStarted = (quiz as Quiz).status.isStarted; diff --git a/Apps/Server/src/controllers/quiz/GetQuestionsController.ts b/Apps/Server/src/controllers/quiz/GetQuestionsController.ts index d1d3795..6dcd191 100644 --- a/Apps/Server/src/controllers/quiz/GetQuestionsController.ts +++ b/Apps/Server/src/controllers/quiz/GetQuestionsController.ts @@ -1,37 +1,32 @@ import { RequestHandler } from 'express'; import { successResponse } from '../../utils/calls'; -import { QUESTIONS_EN, QUESTIONS_DE, LANGUAGES, Language } from '../../constants'; +import { LANGUAGES, Language, QUIZ_NAMES, QuizName } from '../../constants'; import { ParamsDictionary } from 'express-serve-static-core'; import InvalidLanguageError from '../../errors/InvalidLanguageError'; +import QuizManager from '../../models/QuizManager'; +import InvalidQuizNameError from '../../errors/InvalidQuizNameError'; const validateParams = (params: ParamsDictionary) => { - const { lang } = params; + const { lang, quizName } = params; if (!LANGUAGES.includes(lang as Language)) { throw new InvalidLanguageError(); } - return { lang }; + if (!QUIZ_NAMES.includes(quizName as QuizName)) { + throw new InvalidQuizNameError(); + } + + return { lang: lang as Language, quizName: quizName as QuizName }; } -const GetQuestionsController: RequestHandler = (req, res, next) => { +const GetQuestionsController: RequestHandler = async (req, res, next) => { try { - const { lang } = validateParams(req.params); - - let questions; - - switch (lang) { - case Language.EN: - questions = QUESTIONS_EN; - break; - case Language.DE: - questions = QUESTIONS_DE; - break; - default: - throw new InvalidLanguageError(); - } + const { lang, quizName } = validateParams(req.params); + + const questions = await QuizManager.get(quizName, lang); return res.json( successResponse(questions) diff --git a/Apps/Server/src/controllers/quiz/GetQuizNamesController.ts b/Apps/Server/src/controllers/quiz/GetQuizNamesController.ts new file mode 100644 index 0000000..d98fa37 --- /dev/null +++ b/Apps/Server/src/controllers/quiz/GetQuizNamesController.ts @@ -0,0 +1,16 @@ +import { RequestHandler } from 'express'; +import { successResponse } from '../../utils/calls'; +import { QUIZ_NAMES } from '../../constants'; + +const GetQuizNamesController: RequestHandler = async (req, res, next) => { + try { + return res.json( + successResponse(QUIZ_NAMES) + ); + + } catch (err: any) { + next(err); + } +} + +export default GetQuizNamesController; \ No newline at end of file diff --git a/Apps/Server/src/controllers/quiz/StartQuestionController.ts b/Apps/Server/src/controllers/quiz/StartQuestionController.ts index 6d54933..967dac2 100644 --- a/Apps/Server/src/controllers/quiz/StartQuestionController.ts +++ b/Apps/Server/src/controllers/quiz/StartQuestionController.ts @@ -3,14 +3,13 @@ import logger from '../../logger'; import { APP_DB } from '../..'; import { successResponse } from '../../utils/calls'; import { ParamsDictionary } from 'express-serve-static-core'; -import { N_QUESTIONS } from '../../constants'; import { Quiz } from '../../types/QuizTypes'; import InvalidQuizIdError from '../../errors/InvalidQuizIdError'; import InvalidQuestionIndexError from '../../errors/InvalidQuestionIndexError'; import UserCannotStartQuestionError from '../../errors/UserCannotStartQuestionError'; import UserCannotStartUnsupervisedQuestionError from '../../errors/UserCannotStartUnsupervisedQuestionError'; -import PlayersNotReadyError from '../../errors/PlayersNotReadyError'; import InvalidParamsError from '../../errors/InvalidParamsError'; +import QuizManager from '../../models/QuizManager'; const validateParams = async (params: ParamsDictionary) => { const { quizId, questionIndex: _questionIndex } = params; @@ -19,12 +18,13 @@ const validateParams = async (params: ParamsDictionary) => { throw new InvalidParamsError(); } - if (!await APP_DB.doesQuizExist(quizId)) { + const quiz = await APP_DB.getQuiz(quizId); + if (!quiz) { throw new InvalidQuizIdError(); } const questionIndex = Number(_questionIndex); - const isQuestionIndexValid = 0 <= questionIndex && questionIndex < N_QUESTIONS; + const isQuestionIndexValid = 0 <= questionIndex && questionIndex < QuizManager.count(quiz.name); if (!isQuestionIndexValid) { throw new InvalidQuestionIndexError(); } @@ -69,7 +69,7 @@ const StartQuestionController: RequestHandler = async (req, res, next) => { // } await APP_DB.incrementQuestionIndex(quizId); - logger.info(`Question ${questionIndex + 1}/${N_QUESTIONS} of quiz '${quizId}' has been started by admin '${username}'.`); + logger.info(`Question ${questionIndex + 1}/${QuizManager.count(quiz.name)} of quiz '${quizId}' has been started by admin '${username}'.`); return res.json(successResponse()); diff --git a/Apps/Server/src/controllers/quiz/VoteController.ts b/Apps/Server/src/controllers/quiz/VoteController.ts index 1ea5411..0f77a5a 100644 --- a/Apps/Server/src/controllers/quiz/VoteController.ts +++ b/Apps/Server/src/controllers/quiz/VoteController.ts @@ -4,10 +4,10 @@ import { APP_DB } from '../..'; import { successResponse } from '../../utils/calls'; import { Quiz, Vote } from '../../types/QuizTypes'; import { ParamsDictionary } from 'express-serve-static-core'; -import { N_QUESTIONS } from '../../constants'; import InvalidQuizIdError from '../../errors/InvalidQuizIdError'; import InvalidParamsError from '../../errors/InvalidParamsError'; import InvalidQuestionIndexError from '../../errors/InvalidQuestionIndexError'; +import QuizManager from '../../models/QuizManager'; const validateParams = async (params: ParamsDictionary) => { const { quizId, questionIndex: _questionIndex } = params; @@ -16,12 +16,13 @@ const validateParams = async (params: ParamsDictionary) => { throw new InvalidParamsError(); } - if (!await APP_DB.doesQuizExist(quizId)) { + const quiz = await APP_DB.getQuiz(quizId); + if (!quiz) { throw new InvalidQuizIdError(); } const questionIndex = Number(_questionIndex); - const isQuestionIndexValid = 0 <= questionIndex && questionIndex < N_QUESTIONS; + const isQuestionIndexValid = 0 <= questionIndex && questionIndex < QuizManager.count(quiz.name); if (!isQuestionIndexValid) { throw new InvalidQuestionIndexError(); } @@ -68,12 +69,12 @@ const VoteController: RequestHandler = async (req, res, next) => { // That was not the last question and the game is not supervised: // the index can be automatically incremented - if (questionIndex + 1 < N_QUESTIONS && !quiz.status.isSupervised) { + if (questionIndex + 1 < QuizManager.count(quiz.name) && !quiz.status.isSupervised) { await APP_DB.incrementQuestionIndex(quizId); } // That was the last question: the game is now over - else if (questionIndex + 1 === N_QUESTIONS) { + else if (questionIndex + 1 === QuizManager.count(quiz.name)) { await APP_DB.finishQuiz(quizId); } } diff --git a/Apps/Server/src/errors/InvalidQuizNameError.ts b/Apps/Server/src/errors/InvalidQuizNameError.ts new file mode 100644 index 0000000..4701014 --- /dev/null +++ b/Apps/Server/src/errors/InvalidQuizNameError.ts @@ -0,0 +1,11 @@ +class InvalidQuizNameError extends Error { + constructor() { + super('INVALID_QUIZ_NAME'); + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export default InvalidQuizNameError; \ No newline at end of file diff --git a/Apps/Server/src/models/QuizManager.ts b/Apps/Server/src/models/QuizManager.ts new file mode 100644 index 0000000..ce5f8da --- /dev/null +++ b/Apps/Server/src/models/QuizManager.ts @@ -0,0 +1,54 @@ +import { LANGUAGES, Language, QUIZ_NAMES, QuizName } from '../constants'; +import { QuizJSON } from '../types/JSONTypes'; + +type QuizDirectory = Record>; + +class QuizManager { + private static instance: QuizManager; + + private quizzes: QuizDirectory; + + private constructor() { + this.quizzes = LANGUAGES.reduce((prevLang, currLang) => { + return { + ...prevLang, + [currLang]: QUIZ_NAMES.reduce((prev, curr) => { + return { + ...prev, + [curr]: null, + }; + }, {}) + } + }, {} as QuizDirectory); + } + + public static getInstance() { + if (!QuizManager.instance) { + QuizManager.instance = new QuizManager(); + } + return QuizManager.instance; + } + + private async load(name: QuizName, lang: Language) { + this.quizzes[lang][name] = (await import(`../../data/${lang}/${name}.json`)).default; + } + + public async get(name: QuizName, lang: Language = Language.EN) { + if (this.quizzes[lang][name] === null) { + await this.load(name, lang); + } + return this.quizzes[lang][name] as QuizJSON; + } + + public count(name: QuizName) { + const json = this.quizzes[Language.EN][name]; + + if (json === null) { + throw new Error('QUIZ_DATA_NOT_LOADED'); + } + + return Object.keys(json).length; + } +} + +export default QuizManager.getInstance(); \ No newline at end of file diff --git a/Apps/Server/src/models/databases/AppDatabase.ts b/Apps/Server/src/models/databases/AppDatabase.ts index 951eb69..5e0c894 100644 --- a/Apps/Server/src/models/databases/AppDatabase.ts +++ b/Apps/Server/src/models/databases/AppDatabase.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcrypt'; import { N_SALT_ROUNDS } from '../../config'; -import { N_QUESTIONS, ANSWERS_EN } from '../../constants'; +import { QUIZ_NAMES, QuizName } from '../../constants'; import logger from '../../logger'; import { getLast, getRange, unique } from '../../utils/array'; import { sum } from '../../utils/math'; @@ -12,6 +12,8 @@ import QuizAlreadyExistsError from '../../errors/QuizAlreadyExistsError'; import HashError from '../../errors/HashError'; import InvalidQuizIdError from '../../errors/InvalidQuizIdError'; import InvalidQuestionIndexError from '../../errors/InvalidQuestionIndexError'; +import QuizManager from '../QuizManager'; +import InvalidQuizNameError from '../../errors/InvalidQuizNameError'; const SEPARATOR = '|'; @@ -74,14 +76,19 @@ class AppDatabase extends RedisDatabase { return user; } - public async createQuiz(quizId: string, username: string) { + public async createQuiz(quizId: string, quizName: QuizName, username: string) { logger.trace(`Creating a new quiz...`); + if (!QUIZ_NAMES.includes(quizName)) { + throw new InvalidQuizNameError(); + } + if (await this.doesQuizExist(quizId)) { throw new QuizAlreadyExistsError(); } const quiz: Quiz = { + name: quizName, creator: username.toLowerCase(), players: [], status: { @@ -194,7 +201,13 @@ class AppDatabase extends RedisDatabase { } public async getVotesCount(quizId: string) { - const votesCount = new Array(N_QUESTIONS).fill(0); + const quiz = await this.getQuiz(quizId); + + if (!quiz) { + throw new InvalidQuizIdError(); + } + + const votesCount = new Array(QuizManager.count(quiz.name)).fill(0); const votes = await this.getAllVotes(quizId); const players = Object.keys(votes); @@ -230,11 +243,20 @@ class AppDatabase extends RedisDatabase { } public async getAllScores(quizId: string) { + const quiz = await this.getQuiz(quizId); + + if (!quiz) { + throw new InvalidQuizIdError(); + } + + const questions = await QuizManager.get(quiz.name); + const answers = questions.map((question) => question.answer); + const scores = Object .entries(await this.getAllVotes(quizId)) .reduce((prev, [player, votes]) => { const score = sum( - ANSWERS_EN + answers .map((answerIndex, i) => i < votes.length && answerIndex === votes[i]) .map(Number) ); @@ -300,9 +322,15 @@ class AppDatabase extends RedisDatabase { } public async incrementQuestionIndex(quizId: string) { + const quiz = await this.getQuiz(quizId); + + if (!quiz) { + throw new InvalidQuizIdError(); + } + const questionIndex = await this.getQuestionIndex(quizId); - if (questionIndex + 1 > N_QUESTIONS) { + if (questionIndex + 1 > QuizManager.count(quiz.name)) { throw new InvalidQuestionIndexError(); } diff --git a/Apps/Server/src/routes/RouterAPI.ts b/Apps/Server/src/routes/RouterAPI.ts index bcd95b1..aeddcea 100644 --- a/Apps/Server/src/routes/RouterAPI.ts +++ b/Apps/Server/src/routes/RouterAPI.ts @@ -16,6 +16,8 @@ import StartQuestionController from '../controllers/quiz/StartQuestionController import DeleteQuizController from '../controllers/quiz/DeleteQuizController'; import DeleteDatabaseController from '../controllers/DeleteDatabaseController'; import GetVersionController from '../controllers/app/GetVersionController'; +import GetQuizNamesController from '../controllers/quiz/GetQuizNamesController'; +import GetBackgroundUrlController from '../controllers/app/GetBackgroundUrlController'; @@ -38,18 +40,21 @@ router.get('/auth', [AuthMiddleware], PingController); router.delete('/auth', [AuthMiddleware], LogoutController); router.get('/version', GetVersionController); - +router.get('/bg/:quizName', GetBackgroundUrlController); router.get('/user', [AuthMiddleware], GetUserController); -router.get('/questions/:lang', GetQuestionsController); - router.get('/quiz/:quizId', [AuthMiddleware], GetStatusController); +router.put('/quiz/:quizId', [AuthMiddleware], StartQuizController); router.delete('/quiz/:quizId', [AuthMiddleware], DeleteQuizController); -router.put('/quiz/:quizId/start', [AuthMiddleware], StartQuizController); + +router.put('/quiz/:quizId/question/:questionIndex', [AuthMiddleware], StartQuestionController); +router.post('/quiz/:quizId/question/:questionIndex', [AuthMiddleware], VoteController); + router.get('/quiz/:quizId/votes', [AuthMiddleware], GetVotesController); router.get('/quiz/:quizId/scores', [AuthMiddleware], GetScoresController); -router.post('/quiz/:quizId/question/:questionIndex', [AuthMiddleware], VoteController); -router.put('/quiz/:quizId/question/:questionIndex/start', [AuthMiddleware], StartQuestionController); + +router.get('/quiz', GetQuizNamesController); +router.get('/questions/:lang/:quizName', GetQuestionsController); diff --git a/Apps/Server/src/types/JSONTypes.ts b/Apps/Server/src/types/JSONTypes.ts new file mode 100644 index 0000000..44856f5 --- /dev/null +++ b/Apps/Server/src/types/JSONTypes.ts @@ -0,0 +1,12 @@ +import { QuestionType } from '../constants'; + +export type QuestionJSON = { + type: QuestionType, + theme: string, + question: string, + options: string[], + answer: number, + url?: string, +}; + +export type QuizJSON = QuestionJSON[]; \ No newline at end of file diff --git a/Apps/Server/src/types/QuizTypes.ts b/Apps/Server/src/types/QuizTypes.ts index 1c2f0ff..b2442fa 100644 --- a/Apps/Server/src/types/QuizTypes.ts +++ b/Apps/Server/src/types/QuizTypes.ts @@ -1,9 +1,12 @@ +import { QuizName } from "../constants"; + export type Vote = { questionIndex: number, vote: number, }; export type Quiz = { + name: QuizName, creator: string, players: string[], status: { diff --git a/README.md b/README.md index 4c0d85d..180830c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Liquors of the World +# Quiz App -Welcome to Liquors of the World! This project is an online quiz web app, which can host as many players as you want. The quiz explores and various liquors from around the world. +This project is an implementation of an online quiz web app, which can host as many players as needed. ## Technologies Used diff --git a/Scripts/build.sh b/Scripts/build.sh index b634574..5c8ea18 100644 --- a/Scripts/build.sh +++ b/Scripts/build.sh @@ -3,7 +3,7 @@ dir="$(cd "$(dirname "$0")" && pwd)" # Define constant image details user="dleclercpro" -app="liquors-quiz" +app="quiz" release="latest" # Build app image diff --git a/Scripts/run.local.sh b/Scripts/run.local.sh index cd6d5a7..8c30984 100644 --- a/Scripts/run.local.sh +++ b/Scripts/run.local.sh @@ -1,6 +1,6 @@ # Define constant image details user="dleclercpro" -app="liquors-quiz" +app="quiz" release="latest" composefile="docker-compose.local.yml" diff --git a/Scripts/run.sh b/Scripts/run.sh index 3b44095..be68355 100644 --- a/Scripts/run.sh +++ b/Scripts/run.sh @@ -1,6 +1,6 @@ # Define constant image details user="dleclercpro" -app="liquors-quiz" +app="quiz" release="latest" composefile="docker-compose.yml" diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 1701cca..d7bff9a 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -2,19 +2,19 @@ version: '3.8' services: - liquors-quiz-app: - image: dleclercpro/liquors-quiz:latest - container_name: liquors-quiz-app + quiz-app: + image: dleclercpro/quiz:latest + container_name: quiz-app ports: - 80:8000 depends_on: - - liquors-quiz-redis + - quiz-redis environment: NODE_ENV: production - liquors-quiz-redis: + quiz-redis: image: redis:latest - container_name: liquors-quiz-redis + container_name: quiz-redis volumes: - redis:/data diff --git a/docker-compose.yml b/docker-compose.yml index a28dff5..2219c3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,18 +2,18 @@ version: '3.8' services: - liquors-quiz-app: - image: dleclercpro/liquors-quiz:latest - container_name: liquors-quiz-app + quiz-app: + image: dleclercpro/quiz:latest + container_name: quiz-app networks: - custom depends_on: - - liquors-quiz-redis + - quiz-redis environment: NODE_ENV: production - liquors-quiz-nginx: - container_name: liquors-quiz-nginx + quiz-nginx: + container_name: quiz-nginx image: dleclercpro/reverse-proxy:latest restart: always ports: # Comment out when getting SSL certs for the first time @@ -25,13 +25,13 @@ services: - letsencrypt:/etc/letsencrypt # Keep SSL certificates - dhparams:/usr/dhparams # Keep DH params file depends_on: - - liquors-quiz-app + - quiz-app env_file: - .env - liquors-quiz-redis: + quiz-redis: image: redis:latest - container_name: liquors-quiz-redis + container_name: quiz-redis restart: always networks: - custom
    {t('COMMON.RANK')}{t('COMMON.USERNAME')}{t('COMMON.SCORE')}{t('common:COMMON.RANK')}{t('common:COMMON.USERNAME')}{t('common:COMMON.SCORE')}