diff --git a/.yarn/cache/@next-swc-win32-x64-msvc-npm-12.3.4-54d10742b1-10.zip b/.yarn/cache/@next-swc-darwin-arm64-npm-12.3.4-3c587df0e7-10.zip similarity index 57% rename from .yarn/cache/@next-swc-win32-x64-msvc-npm-12.3.4-54d10742b1-10.zip rename to .yarn/cache/@next-swc-darwin-arm64-npm-12.3.4-3c587df0e7-10.zip index 16d8449c8..33ac3e251 100644 Binary files a/.yarn/cache/@next-swc-win32-x64-msvc-npm-12.3.4-54d10742b1-10.zip and b/.yarn/cache/@next-swc-darwin-arm64-npm-12.3.4-3c587df0e7-10.zip differ diff --git a/.yarn/cache/@swc-core-win32-x64-msvc-npm-1.2.205-ff0a0ff4f5-10.zip b/.yarn/cache/@swc-core-darwin-arm64-npm-1.2.205-7711ca8be0-10.zip similarity index 70% rename from .yarn/cache/@swc-core-win32-x64-msvc-npm-1.2.205-ff0a0ff4f5-10.zip rename to .yarn/cache/@swc-core-darwin-arm64-npm-1.2.205-7711ca8be0-10.zip index 5c74ea4ff..3b724a4e6 100644 Binary files a/.yarn/cache/@swc-core-win32-x64-msvc-npm-1.2.205-ff0a0ff4f5-10.zip and b/.yarn/cache/@swc-core-darwin-arm64-npm-1.2.205-7711ca8be0-10.zip differ diff --git a/.yarn/cache/fsevents-patch-6b67494872-10.zip b/.yarn/cache/fsevents-patch-6b67494872-10.zip new file mode 100644 index 000000000..9887ada72 Binary files /dev/null and b/.yarn/cache/fsevents-patch-6b67494872-10.zip differ diff --git a/.yarn/cache/fsevents-patch-afc6995412-10.zip b/.yarn/cache/fsevents-patch-afc6995412-10.zip new file mode 100644 index 000000000..34871c571 Binary files /dev/null and b/.yarn/cache/fsevents-patch-afc6995412-10.zip differ diff --git a/server/controllers/activity.ts b/server/controllers/activity.ts index 630c1b93b..e480e133f 100644 --- a/server/controllers/activity.ts +++ b/server/controllers/activity.ts @@ -377,6 +377,24 @@ activityController.put({ path: '/:id', userType: UserType.TEACHER }, async (req: res.sendJSON(activity); }); +// --- create a game --- +const createGame = async (data: GameData, activity: Activity): Promise => { + const id = data.gameId; + const game = id ? await AppDataSource.getRepository(Game).findOneOrFail({ where: { id: data.gameId || 0 } }) : new Game(); + delete data['gameId']; + game.activityId = activity.id; + game.villageId = activity.villageId; + game.userId = activity.userId; + game.type = activity.subType; + game.signification = data.signification; + game.fakeSignification1 = data.fakeSignification1; + game.fakeSignification2 = data.fakeSignification2; + game.origine = data.origine; + game.video = data.video; + await AppDataSource.getRepository(Game).save(game); + return game; +}; + activityController.put({ path: '/:id/askSame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { const user = req.user; if (!user) { @@ -404,24 +422,6 @@ activityController.put({ path: '/:id/askSame', userType: UserType.TEACHER }, asy res.sendJSON(activity); }); -// --- create a game --- -const createGame = async (data: GameData, activity: Activity): Promise => { - const id = data.gameId; - const game = id ? await AppDataSource.getRepository(Game).findOneOrFail({ where: { id: data.gameId || 0 } }) : new Game(); - delete data['gameId']; - game.activityId = activity.id; - game.villageId = activity.villageId; - game.userId = activity.userId; - game.type = activity.subType; - game.signification = data.signification; - game.fakeSignification1 = data.fakeSignification1; - game.fakeSignification2 = data.fakeSignification2; - game.origine = data.origine; - game.video = data.video; - await AppDataSource.getRepository(Game).save(game); - return game; -}; - // --- create a image --- const createStory = async (data: StoryElement, activity: Activity, type: ImageType, inspiredStoryId: number = 0): Promise => { const id = data.imageId; diff --git a/server/controllers/game.ts b/server/controllers/game.ts index 6f0f4ca34..8ad829e39 100644 --- a/server/controllers/game.ts +++ b/server/controllers/game.ts @@ -1,6 +1,9 @@ import type { JSONSchemaType } from 'ajv'; import type { NextFunction, Request, Response } from 'express'; +import type { ActivityContent, AnyData } from '../../types/activity.type'; +import { ActivityStatus } from '../../types/activity.type'; +import { Activity } from '../entities/activity'; import { Game } from '../entities/game'; import { GameResponse } from '../entities/gameResponse'; import { UserType } from '../entities/user'; @@ -156,29 +159,31 @@ gameController.get({ path: '/ableToPlay', userType: UserType.TEACHER }, async (r next(); return; } - const games = await AppDataSource.getRepository(Game) - .createQueryBuilder('game') - .leftJoinAndSelect('game.responses', 'responses') - .andWhere('`game`.`villageId` = :villageId', { villageId: villageId }) - .andWhere('`game`.`type` = :type', { type: type }) - .andWhere( - (qb) => { - const subQuery = qb - .subQuery() - .select() - .from(GameResponse, 'response') - .where(`response.userId = :userId`, { userId: userId }) - .andWhere(`response.gameId = game.id`) - .andWhere(`response.isOldResponse = 0`) - .getQuery(); - return 'NOT EXISTS ' + subQuery; - }, - { userId: req.user.id }, - ) - .getMany(); - res.sendJSON({ - games: games, - }); + if (type === 0) { + const games = await AppDataSource.getRepository(Game) + .createQueryBuilder('game') + .leftJoinAndSelect('game.responses', 'responses') + .andWhere('`game`.`villageId` = :villageId', { villageId: villageId }) + .andWhere('`game`.`type` = :type', { type: type }) + .andWhere( + (qb) => { + const subQuery = qb + .subQuery() + .select() + .from(GameResponse, 'response') + .where(`response.userId = :userId`, { userId: userId }) + .andWhere(`response.gameId = game.id`) + .andWhere(`response.isOldResponse = 0`) + .getQuery(); + return 'NOT EXISTS ' + subQuery; + }, + { userId: req.user.id }, + ) + .getMany(); + res.sendJSON({ + games: games, + }); + } }); //--- retrieve answers to the mimic with this id --- @@ -201,6 +206,7 @@ gameController.get({ path: '/stats/:gameId', userType: UserType.TEACHER }, async type UpdateActivity = { value: string; + villageId: number; }; const ANSWER_M_SCHEMA: JSONSchemaType = { @@ -209,9 +215,12 @@ const ANSWER_M_SCHEMA: JSONSchemaType = { value: { type: 'string', }, + villageId: { + type: 'number', + }, }, required: [], - additionalProperties: false, + additionalProperties: true, }; const answerGameValidator = ajv.compile(ANSWER_M_SCHEMA); @@ -245,11 +254,6 @@ gameController.put({ path: '/play/:id', userType: UserType.TEACHER }, async (req return; } - const game = await AppDataSource.getRepository(Game).findOne({ where: { id: id } }); - if (!game) { - next(); - return; - } const responses = await AppDataSource.getRepository(GameResponse).find({ where: { userId: userId, id: id } }); if (responses.length > 2) { next(); @@ -258,12 +262,219 @@ gameController.put({ path: '/play/:id', userType: UserType.TEACHER }, async (req const gameResponse = new GameResponse(); gameResponse.value = data.value; - gameResponse.gameId = game.id; - gameResponse.villageId = game.villageId; + gameResponse.gameId = id; + gameResponse.villageId = data.villageId; gameResponse.userId = userId; await AppDataSource.getRepository(GameResponse).save(gameResponse); res.sendJSON(GameResponse); }); +//--- Create a standardised game --- + +gameController.post({ path: '/standardGame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + + const data = req.body; + const game1 = data.game1; + const game2 = data.game2; + const game3 = data.game3; + + createGame(game1, data.userId, data.villageId, data.type, data.subType, data.selectedPhase); + createGame(game2, data.userId, data.villageId, data.type, data.subType, data.selectedPhase); + createGame(game3, data.userId, data.villageId, data.type, data.subType, data.selectedPhase); + + res.sendStatus(200); +}); + +async function createGame(data: ActivityContent[], userId: number, villageId: number, type: number, subType: number, selectedPhase: number) { + const activity = new Activity(); + activity.type = type; + activity.subType = subType; + activity.status = ActivityStatus.PUBLISHED; + // TODO: Travailler sur le type de data + activity.data = data as unknown as AnyData; + activity.phase = selectedPhase; + activity.content = data; + activity.userId = userId; + activity.villageId = villageId; + activity.responseActivityId = null; + activity.responseType = null; + activity.isPinned = false; + activity.displayAsUser = false; + activity.publishDate = new Date(); + + await AppDataSource.getRepository(Activity).save(activity); +} + +// --- Get all games standardised by subType --- + +gameController.get({ path: '/allStandardGame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + + const subType = parseInt(getQueryString(req.query.subType) || '0', 10); + const villageId = req.user.type <= UserType.TEACHER ? parseInt(getQueryString(req.query.villageId) || '0', 10) || null : req.user.villageId; + + const subQueryBuilder = AppDataSource.getRepository(Activity) + .createQueryBuilder('activity') + .where('activity.villageId = :villageId', { villageId: villageId }) + .andWhere('activity.type = :type', { type: 4 }) + .andWhere('activity.subType = :subType', { subType: subType }); + + const games = await subQueryBuilder + .orderBy('activity.createDate', 'DESC') + .limit(200) + .offset(0 * 200) + .getMany(); + + res.sendJSON(games); +}); + +// --- Get one game standardised --- + +gameController.get({ path: '/standardGame/:id', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + const id = parseInt(req.params.id, 10) || 0; + const game = await AppDataSource.getRepository(Activity).findOne({ where: { id } }); + if (!game || (req.user.type === UserType.TEACHER && req.user.villageId !== game.villageId)) { + next(); + return; + } + res.sendJSON(game); +}); + +// --- Get one random game standardised to play --- +gameController.get({ path: '/playStandardGame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + const villageId = req.user.type <= UserType.TEACHER ? parseInt(getQueryString(req.query.villageId) || '0', 10) || null : req.user.villageId; + const type = 4; + const subType = parseInt(getQueryString(req.query.subType) || '0', 10); + const game = await AppDataSource.getRepository(Activity) + .createQueryBuilder('activity') + .andWhere('activity.villageId = :villageId', { villageId: villageId }) + .andWhere('activity.type = :type', { type: type }) + .andWhere('activity.subType = :subType', { subType: subType }) + .orderBy('RAND()') + .getOne(); + if (!game) { + next(); + return; + } + res.sendJSON(game); +}); + +// --- Get the last created game standardised --- + +gameController.get({ path: '/latestStandard', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + const villageId = req.user.type <= UserType.TEACHER ? parseInt(getQueryString(req.query.villageId) || '0', 10) || null : req.user.villageId; + const type = 4; + const subType = parseInt(getQueryString(req.query.subType) || '0', 10); + const latestGame = await AppDataSource.getRepository(Activity) + .createQueryBuilder('activity') + .where('activity.villageId = :villageId', { villageId: villageId }) + .andWhere('activity.type = :type', { type: type }) + .andWhere('activity.subType = :subType', { subType: subType }) + .orderBy('activity.createDate', 'DESC') + .getOne(); + + if (!latestGame) { + next(); + return; + } + + res.sendJSON(latestGame); +}); + +// --- Get all games standardised available --- + +gameController.get({ path: '/ableToPlayStandardGame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + const userId = req.user.id; + const villageId = req.user.type <= UserType.TEACHER ? parseInt(getQueryString(req.query.villageId) || '0', 10) || null : req.user.villageId; + const type = 4; + const subType = parseInt(getQueryString(req.query.subType) || '0', 10); + + const games = await AppDataSource.getRepository(Activity) + .createQueryBuilder('activity') + .leftJoin(GameResponse, 'GameResponse') + .andWhere('`activity`.`villageId` = :villageId', { villageId: villageId }) + .andWhere('`activity`.`type` = :type', { type: type }) + .andWhere('`activity`.`subType` = :subType', { subType: subType }) + .andWhere( + (qb) => { + const subQuery = qb + .subQuery() + .select() + .from(GameResponse, 'response') + .where(`response.userId = :userId`, { userId: userId }) + .andWhere(`response.gameId = activity.id`) + .andWhere(`response.isOldResponse = 0`) + .getQuery(); + return 'NOT EXISTS ' + subQuery; + }, + { userId: req.user.id }, + ) + .getMany(); + res.sendJSON({ + activities: games, + }); +}); + +// --- Get number of games standardised available --- + +gameController.get({ path: '/countAbleToPlayStandardGame', userType: UserType.TEACHER }, async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + next(); + return; + } + const userId = req.user.id; + const villageId = req.user.type <= UserType.TEACHER ? parseInt(getQueryString(req.query.villageId) || '0', 10) || null : req.user.villageId; + const type = 4; + const subType = parseInt(getQueryString(req.query.subType) || '0', 10); + + const gameCount = await AppDataSource.getRepository(Activity) + .createQueryBuilder('activity') + .leftJoin(GameResponse, 'GameResponse') + .andWhere('`activity`.`villageId` = :villageId', { villageId: villageId }) + .andWhere('`activity`.`type` = :type', { type: type }) + .andWhere('`activity`.`subType` = :subType', { subType: subType }) + .andWhere( + (qb) => { + const subQuery = qb + .subQuery() + .select() + .from(GameResponse, 'response') + .where(`response.userId = :userId`, { userId: userId }) + .andWhere(`response.gameId = activity.id`) + .andWhere(`response.isOldResponse = 0`) + .getQuery(); + return 'NOT EXISTS ' + subQuery; + }, + { userId: req.user.id }, + ) + .getCount(); + res.sendJSON({ + count: gameCount, + }); +}); + export { gameController }; diff --git a/server/migrations/1710427662589-AlterGameResponseTable.ts b/server/migrations/1710427662589-AlterGameResponseTable.ts new file mode 100644 index 000000000..97d8a8ddc --- /dev/null +++ b/server/migrations/1710427662589-AlterGameResponseTable.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AlterGameResponseTable1710427662589 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE game_response DROP FOREIGN KEY FK_3a0c737a217bbd6bbd268203fb8`); + await queryRunner.query(`ALTER TABLE game_response DROP COLUMN gameId`); + await queryRunner.query(`ALTER TABLE game_response ADD COLUMN gameId INT`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE game_response DROP COLUMN gameId`); + await queryRunner.query(`ALTER TABLE game_response ADD COLUMN gameId INT`); + await queryRunner.query(`ALTER TABLE game_response ADD CONSTRAINT FK_3a0c737a217bbd6bbd268203fb8 FOREIGN KEY (gameId) REFERENCES game(id)`); + } +} diff --git a/server/utils/iso-4217-currencies-french.ts b/server/utils/iso-4217-currencies-french.ts index 00a4bf1ab..1ae3422e2 100644 --- a/server/utils/iso-4217-currencies-french.ts +++ b/server/utils/iso-4217-currencies-french.ts @@ -56,7 +56,7 @@ export const currencies: Currency[] = [ { name: 'Dollars du Zimbabwe', code: 'ZWL', iso: '932' }, { name: 'Dong', code: 'VND', iso: '704' }, { name: 'Dram Armenien', code: 'AMD', iso: '51' }, - { name: 'Euro', code: 'EUR', iso: '978' }, + { name: 'Euros', code: 'EUR', iso: '978' }, { name: 'Florin des Antilles néerlandaises', code: 'ANG', iso: '532' }, { name: 'Forint', code: 'HUF', iso: '348' }, { name: 'Franc Burundi', code: 'BIF', iso: '108' }, diff --git a/server/utils/iso-639-languages-french.ts b/server/utils/iso-639-languages-french.ts index 87871a55e..1bbd3d238 100644 --- a/server/utils/iso-639-languages-french.ts +++ b/server/utils/iso-639-languages-french.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import type { Language } from '../../types/language.type'; -export const languages: Language[] = [ +const language: Language[] = [ { english: 'Afar', french: 'afar', @@ -2852,3 +2852,12 @@ export const languages: Language[] = [ alpha3_t: '', }, ]; + +const toCapitalized = language.map((language) => { + return { + ...language, + french: language.french.charAt(0).toUpperCase() + language.french.slice(1), + }; +}); + +export const languages: Language[] = toCapitalized; diff --git a/src/activity-types/game.constants.ts b/src/activity-types/game.constants.ts index 57d1d4c49..215c3fe09 100644 --- a/src/activity-types/game.constants.ts +++ b/src/activity-types/game.constants.ts @@ -1,4 +1,13 @@ -import type { MimicsData, GameMimicActivity, GameMoneyActivity, GameActivity, MimicData } from '../../types/game.type'; +import type { + MimicsData, + ExpressionsData, + GameMimicActivity, + GameExpressionActivity, + GameMoneyActivity, + GameActivity, + MimicData, + ExpressionData, +} from '../../types/game.type'; import { GameType } from '../../types/game.type'; export const DEFAULT_MIMIC_DATA: MimicsData = { @@ -31,6 +40,37 @@ export const DEFAULT_MIMIC_DATA: MimicsData = { }, }; +export const DEFAULT_EXPRESSION_DATA: ExpressionsData = { + langage: null, + game1: { + gameId: null, + createDate: null, + origine: null, + signification: null, + fakeSignification1: null, + fakeSignification2: null, + video: null, + }, + game2: { + gameId: null, + createDate: null, + origine: null, + signification: null, + fakeSignification1: null, + fakeSignification2: null, + video: null, + }, + game3: { + gameId: null, + createDate: null, + origine: null, + signification: null, + fakeSignification1: null, + fakeSignification2: null, + video: null, + }, +}; + export const isMimicValid = (data: MimicData): boolean => { return ( data.signification != null && @@ -44,9 +84,25 @@ export const isMimicValid = (data: MimicData): boolean => { ); }; +export const isExpressionValid = (data: ExpressionData): boolean => { + return ( + data.signification != null && + data.signification.length > 0 && + data.fakeSignification1 != null && + data.fakeSignification1.length > 0 && + data.fakeSignification2 != null && + data.fakeSignification2.length > 0 && + data.video != null && + data.video.length > 0 + ); +}; + export const isMimic = (activity: GameActivity): activity is GameMimicActivity => { return activity.subType === GameType.MIMIC; }; export const isMoney = (activity: GameActivity): activity is GameMoneyActivity => { return activity.subType === GameType.MONEY; }; +export const isExpression = (activity: GameActivity): activity is GameExpressionActivity => { + return activity.subType === GameType.EXPRESSION; +}; diff --git a/src/api/currencie/currencies.get.ts b/src/api/currencie/currencies.get.ts new file mode 100644 index 000000000..347331aa6 --- /dev/null +++ b/src/api/currencie/currencies.get.ts @@ -0,0 +1,23 @@ +import { useQuery } from 'react-query'; + +import { axiosRequest } from 'src/utils/axiosRequest'; +import type { Currency } from 'types/currency.type'; + +export const getCurrencies = async (): Promise => { + const response = await axiosRequest({ + method: 'GET', + url: '/currencies', + }); + if (response.error) { + return []; + } + return response.data; +}; + +export const useCurrencies = (): { currencies: Currency[] } => { + const { data, isLoading, error } = useQuery(['currencies'], getCurrencies); + + return { + currencies: isLoading || error ? [] : data || [], + }; +}; diff --git a/src/api/game/game.getAllBySubtype.ts b/src/api/game/game.getAllBySubtype.ts new file mode 100644 index 000000000..410cb9605 --- /dev/null +++ b/src/api/game/game.getAllBySubtype.ts @@ -0,0 +1,25 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; + +type GetAllStandardGameByTypeProps = { + subType: number; + villageId: number | undefined; +}; + +export async function getAllStandardGameByType({ subType, villageId }: GetAllStandardGameByTypeProps) { + const path = `/allStandardGame${serializeToQueryUrl({ + subType, + villageId, + })}`; + const response = await axiosRequest({ + method: 'GET', + url: `/games${path}`, + }); + return response.error ? undefined : response.data; +} + +export function useAllStandardGameByType(subType: number, villageId: number) { + return useQuery(['allStandardGameByType', { subType, villageId }], () => getAllStandardGameByType({ subType, villageId })); +} diff --git a/src/api/game/game.getAllGames.ts b/src/api/game/game.getAllGames.ts new file mode 100644 index 000000000..a53baaf32 --- /dev/null +++ b/src/api/game/game.getAllGames.ts @@ -0,0 +1,25 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; + +type GetCountAllStandardGameProps = { + subType: number; + villageId: number; +}; + +export async function getCountAllStandardGame({ subType, villageId }: GetCountAllStandardGameProps) { + const path = `/allStandardGame${serializeToQueryUrl({ + subType, + villageId, + })}`; + const response = await axiosRequest({ + method: 'GET', + url: `/games${path}`, + }); + return response.error ? undefined : (response.data.length as number); +} + +export function useCountAllStandardGame(subType: number, villageId: number) { + return useQuery(['allGames', { subType, villageId }], () => getCountAllStandardGame({ subType, villageId })); +} diff --git a/src/api/game/game.getAvailable.ts b/src/api/game/game.getAvailable.ts new file mode 100644 index 000000000..9c75e32a2 --- /dev/null +++ b/src/api/game/game.getAvailable.ts @@ -0,0 +1,25 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; + +type GetAbleToPlayStandardGameProps = { + subType: number; + villageId: number; +}; + +export async function getAbleToPlayStandardGame({ subType, villageId }: GetAbleToPlayStandardGameProps) { + const path = `AbleToPlayStandardGame${serializeToQueryUrl({ + subType, + villageId, + })}`; + const response = await axiosRequest({ + method: 'GET', + url: `/games/${path}`, + }); + return response.error ? undefined : response.data.activities; +} + +export function useAbleToPlayStandardGame(subType: number, villageId: number) { + return useQuery(['AbleToPlay', { subType, villageId }], () => getAbleToPlayStandardGame({ subType, villageId })); +} diff --git a/src/api/game/game.getCountAvailable.ts b/src/api/game/game.getCountAvailable.ts new file mode 100644 index 000000000..08e289793 --- /dev/null +++ b/src/api/game/game.getCountAvailable.ts @@ -0,0 +1,25 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; + +type GetCountAbleToPlayStandardGameProps = { + subType: number; + villageId: number; +}; + +export async function getCountAbleToPlayStandardGame({ subType, villageId }: GetCountAbleToPlayStandardGameProps) { + const path = `countAbleToPlayStandardGame${serializeToQueryUrl({ + subType, + villageId, + })}`; + const response = await axiosRequest({ + method: 'GET', + url: `/games/${path}`, + }); + return response.error ? undefined : response.data.count; +} + +export function useCountAbleToPlayStandardGame(subType: number, villageId: number) { + return useQuery(['countAbleToPlay', { subType, villageId }], () => getCountAbleToPlayStandardGame({ subType, villageId })); +} diff --git a/src/api/game/game.getOneGameById.ts b/src/api/game/game.getOneGameById.ts new file mode 100644 index 000000000..f6b478d66 --- /dev/null +++ b/src/api/game/game.getOneGameById.ts @@ -0,0 +1,52 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; +// import { GameType } from 'types/game.type'; + +type Input = { + selectedValue: string; + response: boolean; + type: number; +}; + +type GameItem = { + inputs: Input[]; +}; + +type Data = { + game: GameItem[]; + labelPresentation: string; + language?: string; + monney?: string; + radio: string; +}; + +type DataUse = { + content: Data; + createDate: string; + id: number; + userId: number; + villageId: number; + subType: number; +}; + +type GetOneGameByIdProps = { + subType: number; + id: number; +}; +// TODO : remove specific mimic management after mimic is standard +export async function getOneGameById({ subType, id }: GetOneGameByIdProps) { + const path = `/standardGame/${id}${serializeToQueryUrl({ + subType, + })}`; + const response = await axiosRequest({ + method: 'GET', + url: `/games${path}`, + }); + return response.error ? undefined : (response.data as DataUse); +} + +export function useOneGameById(subType: number, id: number) { + return useQuery(['getOneGameById', { subType, id }], () => getOneGameById({ subType, id })); +} diff --git a/src/api/game/game.latestStandard.ts b/src/api/game/game.latestStandard.ts new file mode 100644 index 000000000..04b5166c5 --- /dev/null +++ b/src/api/game/game.latestStandard.ts @@ -0,0 +1,22 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; + +type GET_SUBTYPE = { + subType: number; +}; + +export async function getType({ subType }: GET_SUBTYPE) { + const response = await axiosRequest({ + method: 'GET', + url: `/games/latestStandard${serializeToQueryUrl({ + subType, + })}`, + }); + return response.error ? undefined : response.data; +} + +export function useType({ subType }: GET_SUBTYPE) { + return useQuery(['getLatest', { subType }], () => getType({ subType })); +} diff --git a/src/api/game/game.post.ts b/src/api/game/game.post.ts new file mode 100644 index 000000000..95f330794 --- /dev/null +++ b/src/api/game/game.post.ts @@ -0,0 +1,17 @@ +import { axiosRequest } from 'src/utils/axiosRequest'; +import type { GameDataMonneyOrExpression } from 'types/game.type'; + +// TODO: changer les noms du type et de la fonction une fois que Mimique sera standardisé aussi + +export async function postGameDataMonneyOrExpression(data: GameDataMonneyOrExpression): Promise { + const response = await axiosRequest({ + method: 'POST', + url: '/games/standardGame', + data, + }); + if (response.error) { + throw new Error('Erreur lors de la création du jeu. Veuillez réessayer.'); + } + // inviladate le cache de la liste des jeux + return response.data; +} diff --git a/src/api/game/game.updateGameResponse.ts b/src/api/game/game.updateGameResponse.ts new file mode 100644 index 000000000..6bf9d56e2 --- /dev/null +++ b/src/api/game/game.updateGameResponse.ts @@ -0,0 +1,35 @@ +import { useQuery } from 'react-query'; + +import { serializeToQueryUrl } from 'src/utils'; +import { axiosRequest } from 'src/utils/axiosRequest'; +import type { GameResponseValue } from 'types/gameResponse.type'; +// import { GameType } from 'types/game.type'; + +type GetOneGameByIdProps = { + value: GameResponseValue; + id: number; +}; + +// TODO : remove specific mimic management after mimic is standard +export async function putUpdateGameResponse({ value, id }: GetOneGameByIdProps) { + const path = `/standardPlay/${id}${serializeToQueryUrl({ + id, + value, + })}`; + const response = await axiosRequest({ + method: 'PUT', + url: `/games${path}`, + data: { + value, + id, + }, + }); + if (response.error) { + return false; + } + return true; +} + +export function useUpdateGameResponse(value: GameResponseValue, id: number) { + return useQuery(['updateResponseGame', { value, id }], () => putUpdateGameResponse({ value, id })); +} diff --git a/src/api/language/languages.get.ts b/src/api/language/languages.get.ts new file mode 100644 index 000000000..da865cdfa --- /dev/null +++ b/src/api/language/languages.get.ts @@ -0,0 +1,23 @@ +import { useQuery } from 'react-query'; + +import { axiosRequest } from 'src/utils/axiosRequest'; +import type { Language } from 'types/language.type'; + +export const getLanguages = async (): Promise => { + const response = await axiosRequest({ + method: 'GET', + url: '/languages', + }); + if (response.error) { + return []; + } + return response.data; +}; + +export const useLanguages = (): { languages: Language[] } => { + const { data, isLoading, error } = useQuery(['languages'], getLanguages); + + return { + languages: isLoading || error ? [] : data || [], + }; +}; diff --git a/src/components/accueil/Accueil.tsx b/src/components/accueil/Accueil.tsx index 64d2bc485..411ee1137 100644 --- a/src/components/accueil/Accueil.tsx +++ b/src/components/accueil/Accueil.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from '@mui/material'; -import { filterActivitiesByTerm, filterActivitiesWithLastMimicGame } from './Filters/FilterActivities'; +import { filterActivitiesByTerm, filterActivitiesWithLastGame } from './Filters/FilterActivities'; import { LinkChild } from './LinkChild'; import { getUserVisibilityFamilyParams } from 'src/api/user/user.get'; import { Base } from 'src/components/Base'; @@ -15,6 +15,8 @@ import { UserContext } from 'src/contexts/userContext'; import { VillageContext } from 'src/contexts/villageContext'; import { useActivities } from 'src/services/useActivities'; import PelicoReflechit from 'src/svg/pelico/pelico_reflechit.svg'; +import type { Activity, AnyData } from 'types/activity.type'; +import { GameType } from 'types/game.type'; import { UserType } from 'types/user.type'; export const Accueil = () => { @@ -64,16 +66,23 @@ export const Accueil = () => { })); }, [selectedPhase]); - //Preload of the activities filtered only one mimic + //Preload of the activities filtered only one by game const activitiesFiltered = React.useMemo(() => { if (activities && activities.length > 0) { - const activitiesWithLastMimic = filterActivitiesWithLastMimicGame(activities); + let activitiesWithLastGame: Activity[] = []; + Object.values(GameType) + .filter((t) => typeof t === 'number') + .map((type) => { + activitiesWithLastGame = filterActivitiesWithLastGame( + activitiesWithLastGame.length === 0 ? activities : activitiesWithLastGame, + type as number, + ); + }); const activitiesFilterBySearchTerm = - filters.searchTerm.length > 0 ? filterActivitiesByTerm(activitiesWithLastMimic, filters.searchTerm) : activitiesWithLastMimic; + filters.searchTerm.length > 0 ? filterActivitiesByTerm(activitiesWithLastGame, filters.searchTerm) : activitiesWithLastGame; return activitiesFilterBySearchTerm; - } else { - return []; } + return []; }, [activities, filters.searchTerm]); if (user && user.type === UserType.FAMILY && !user.hasStudentLinked) { diff --git a/src/components/accueil/Filters/FilterActivities.tsx b/src/components/accueil/Filters/FilterActivities.tsx index 8a31437e8..4960f8579 100644 --- a/src/components/accueil/Filters/FilterActivities.tsx +++ b/src/components/accueil/Filters/FilterActivities.tsx @@ -1,15 +1,15 @@ import { getSubtype, getType } from 'src/activity-types/activity.constants'; import { isGame } from 'src/activity-types/anyActivity'; -import { isMimic } from 'src/activity-types/game.constants'; import type { Activity, AnyData } from 'types/activity.type'; +import { GameType } from 'types/game.type'; -export function filterActivitiesWithLastMimicGame(activitiesData: Activity[]): Activity[] { - const indexOfLastMimic = activitiesData.findIndex((activity) => isGame(activity) && isMimic(activity)); // Get the index of this last mimic +export function filterActivitiesWithLastGame(activitiesData: Activity[], subType: number = GameType.MIMIC): Activity[] { + const indexOfLastMimic = activitiesData.findIndex((activity) => isGame(activity) && activity.subType === subType); // Get the index of this last mimic if (indexOfLastMimic === -1) { return activitiesData; } const mostRecentMimic = activitiesData[indexOfLastMimic]; // Get the last mimic created - const activitiesWithoutMimic = activitiesData.filter((activity) => !isGame(activity) || !isMimic(activity)); // Remove all mimics in activities + const activitiesWithoutMimic = activitiesData.filter((activity) => !isGame(activity) || activity.subType !== subType); // Remove all mimics in activities const activitiesWithLastMimic = [...activitiesWithoutMimic]; activitiesWithLastMimic.splice(indexOfLastMimic, 0, mostRecentMimic); // Put the last mimic created at the same spot in the array diff --git a/src/components/activities/ActivityCard/GameCard.tsx b/src/components/activities/ActivityCard/GameCard.tsx new file mode 100644 index 000000000..46470bf80 --- /dev/null +++ b/src/components/activities/ActivityCard/GameCard.tsx @@ -0,0 +1,203 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import router from 'next/router'; +import React, { useState, useEffect } from 'react'; +import ReactPlayer from 'react-player'; + +import { Button } from '@mui/material'; + +import { CommentIcon } from './CommentIcon'; +import type { ActivityCardProps } from './activity-card.types'; +import { useCountAllStandardGame } from 'src/api/game/game.getAllGames'; +import { useCountAbleToPlayStandardGame } from 'src/api/game/game.getCountAvailable'; +import { RedButton } from 'src/components/buttons/RedButton'; +import { bgPage } from 'src/styles/variables.const'; +import { LinkNotAllowedInPath } from 'types/activity.type'; +import { GameType } from 'types/game.type'; +import type { GameActivity } from 'types/game.type'; + +// TODO : Remove all ts-ignore when mimic is standardized + +type GameCardProps = ActivityCardProps & { + gameType: GameType; +}; + +const TYPE_OF_GAME = { + [GameType.MIMIC]: 'mimique', + [GameType.MONEY]: 'objet', + [GameType.EXPRESSION]: 'expression', +}; + +const phrase = { + [GameType.MIMIC]: { + phrase: 'a relancé le jeu des mimiques', + }, + [GameType.MONEY]: { + phrase: 'a relancé le jeu de la monnaie', + }, + [GameType.EXPRESSION]: { + phrase: 'a relancé le jeu des expressions', + }, +}; + +export const GameCard = ({ activity, isSelf, noButtons, isDraft, showEditButtons, onDelete, gameType }: GameCardProps) => { + const [totalGamesCount, setTotalGamesCount] = useState(0); + const [availableGamesCount, setAvailableGamesCount] = useState(0); + const { data: countAbleToPlay } = useCountAbleToPlayStandardGame(gameType, activity.villageId); + const { data: countAllStandardGame } = useCountAllStandardGame(gameType, activity.villageId); + const typeOfGame = TYPE_OF_GAME[gameType]; + const path = `/creer-un-jeu/${typeOfGame}/displayList`; + const displayPhrasesByType = phrase[gameType]; + + useEffect(() => { + if (countAbleToPlay && countAllStandardGame) { + setAvailableGamesCount(countAbleToPlay); + setTotalGamesCount(countAllStandardGame); + } + }, [countAbleToPlay, countAllStandardGame, gameType]); + const labelPresentation = activity.data.presentation || activity.data.labelPresentation; + + return ( +
+ {path && ( +
+
+ {/* Link is disabled for reaction activity */} + {router.pathname.includes(LinkNotAllowedInPath.REACTION) ? ( + <> + {activity.subType === 0 && ( + + )} + {activity.subType === 1 && ( + <> + {/* eslint-disable-next-line */} + {/* @ts-ignore */} + + + )} + {activity.subType === 2 && ( + <> + {/* eslint-disable-next-line */} + {/* @ts-ignore */} + + + )} + + ) : ( + <> + {activity.subType === 0 && ( + + + + )} + {activity.subType === 1 && ( + + {/* eslint-disable-next-line */} + {/* @ts-ignore */} + + + )} + {activity.subType === 2 && ( + + {/* eslint-disable-next-line */} + {/* @ts-ignore */} + + + )} + + )} +
+
+ )} +
+

+ {labelPresentation} {displayPhrasesByType.phrase} +

+

+ Il y a actuellement {`${totalGamesCount} ${typeOfGame}${totalGamesCount > 1 ? 's' : ''} disponible${totalGamesCount > 1 ? 's' : ''}`}.
+ {availableGamesCount > 0 + ? ` Il en reste ${availableGamesCount} à découvrir.` + : ` Il n'y a pour l'instant pas de nouvel ajout dans la catégorie des ${typeOfGame}s à découvrir.`} +

+ + {noButtons || ( +
+ {!showEditButtons && ( + <> + + + + + + )} + {isSelf && showEditButtons && ( + <> + {activity.subType !== 1 && activity.subType !== 2 && ( + <> + + + + + )} + + Supprimer + + + )} +
+ )} +
+
+ ); +}; diff --git a/src/components/activities/ActivityCard/GameCardMaClasse.tsx b/src/components/activities/ActivityCard/GameCardMaClasse.tsx new file mode 100644 index 000000000..27e364aad --- /dev/null +++ b/src/components/activities/ActivityCard/GameCardMaClasse.tsx @@ -0,0 +1,256 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import router from 'next/router'; +import React from 'react'; +import ReactPlayer from 'react-player'; + +import { Button, Paper } from '@mui/material'; + +import { titles, REACTIONS, icons } from '../utils'; +import { CommentIcon } from './CommentIcon'; +import type { ActivityCardProps } from './activity-card.types'; +import { isReaction } from 'src/activity-types/anyActivity'; +import { AvatarImg } from 'src/components/Avatar'; +import { Flag } from 'src/components/Flag'; +import { UserDisplayName } from 'src/components/UserDisplayName'; +import { RedButton } from 'src/components/buttons/RedButton'; +import { useActivity } from 'src/services/useActivity'; +import { bgPage, primaryColor } from 'src/styles/variables.const'; +import PelicoNeutre from 'src/svg/pelico/pelico_neutre.svg'; +import { toDate } from 'src/utils'; +import { LinkNotAllowedInPath } from 'types/activity.type'; +import { GameType } from 'types/game.type'; +import type { GameActivity } from 'types/game.type'; +import { UserType } from 'types/user.type'; + +// TODO : Remove all ts-ignore when mimic is standardized + +type GameCardProps = ActivityCardProps & { + gameType: GameType; +}; + +const TYPE_OF_GAME = { + [GameType.MIMIC]: 'mimique', + [GameType.MONEY]: 'objet', + [GameType.EXPRESSION]: 'expression', +}; + +export const GameCardMaClasse = ({ + activity, + user, + isSelf = true, + noButtons = false, + showEditButtons = true, + isDraft = false, + forComment = false, + noMargin = false, + onDelete = () => {}, + onSelect, + gameType, +}: GameCardProps) => { + const userIsPelico = (user?.type ?? UserType.MEDIATOR) <= UserType.MEDIATOR; + const { activity: responseActivity } = useActivity(activity.responseActivityId ?? -1); + const ActivityIcon = icons[activity.type] || null; + + const typeOfGame = TYPE_OF_GAME[gameType]; + const path = `/creer-un-jeu/${typeOfGame}/jouer/${activity.id}`; + + // eslint-disable-next-line + // @ts-ignore + const media = activity.content.game[0].inputs[0].selectedValue || ''; + // eslint-disable-next-line + // @ts-ignore + const responseToDisplay = activity.content.game[0].inputs[1].selectedValue.toLowerCase() || ''; + // eslint-disable-next-line + // @ts-ignore + const origine = activity.content.game[0].inputs[2].selectedValue || ''; + // eslint-disable-next-line + // @ts-ignore + const language = activity.content.language || ''; + // eslint-disable-next-line + // @ts-ignore + const monney = activity.content.monney || ''; + + return ( + <> + {activity.subType === 1 || activity.subType === 2 || activity.subType === 0 ? ( +
+ { + if (onSelect !== undefined) { + onSelect(); + } + }} + style={{ + margin: noMargin || forComment ? '0' : '1rem 0', + cursor: onSelect !== undefined ? 'pointer' : 'unset', + border: activity?.isPinned ? `2px solid ${primaryColor}` : undefined, + }} + > +
+ +
+

+ + {' a '} + {responseActivity && isReaction(activity) ? ( + + {titles[activity.type]} {REACTIONS[responseActivity?.type]} + + ) : ( + {titles[activity.type]} + )} +

+
+

Publié le {toDate(activity.publishDate as string)}

+ {userIsPelico ? ( + + + + ) : ( + + )} +
+
+ {ActivityIcon && !isReaction(activity) && ( + + )} +
+
+
+
+ {/* Link is disabled for reaction activity */} + {router.pathname.includes(LinkNotAllowedInPath.REACTION) ? ( + <> + {' '} + {activity.subType === 0 && } + {activity.subType === 1 && } + {activity.subType === 2 && } + + ) : ( + <> + {activity.subType === 0 && ( + + + + )} + {activity.subType === 1 && ( + + + + )} + {activity.subType === 2 && ( + + + + )} + + )} +
+
+
+

Vous avez créé un jeu des {typeOfGame}s

+ {activity.subType === 0 && ( +

+ Vous avez choisi de faire deviner la mimique suivante : {responseToDisplay} +
+ {origine.lenght > 1 && Et voici son origine : {origine}} +

+ )} + {activity.subType === 1 && ( +

+ Votre jeu a été crée en utilisant la monnaie : {monney} et vous avez choisi de faire deviner combien coute cet objet :{' '} + {responseToDisplay} +

+ )} + {activity.subType === 2 && ( +

+ Votre jeu a été crée en utilisant la langue : {language} et vous avez choisi de faire deviner cette expression :{' '} + {responseToDisplay}
+ {origine.lenght > 1 && Et voici la traduction : {origine}} +

+ )} + {noButtons || ( +
+ {!showEditButtons && ( + <> + + + + + + )} + {isSelf && showEditButtons && ( + <> + {activity.subType !== 1 && activity.subType !== 2 && activity.subType !== 0 && ( + <> + + + + + )} + + Supprimer + + + )} +
+ )} +
+
+
+
+ ) : null} + + ); +}; diff --git a/src/components/activities/ActivityCard/MimicCard.tsx b/src/components/activities/ActivityCard/MimicCard.tsx index cc2a482c9..601ed2b8b 100644 --- a/src/components/activities/ActivityCard/MimicCard.tsx +++ b/src/components/activities/ActivityCard/MimicCard.tsx @@ -15,6 +15,8 @@ import { LinkNotAllowedInPath } from 'types/activity.type'; import type { GameActivity } from 'types/game.type'; import { GameType } from 'types/game.type'; +// todo : add game type in props + export const MimicCard = ({ activity, isSelf, noButtons, isDraft, showEditButtons, onDelete }: ActivityCardProps) => { const { village } = React.useContext(VillageContext); const { getAllGamesByType, getAvailableGamesCount } = useGameRequests(); diff --git a/src/components/activities/ActivityCard/index.tsx b/src/components/activities/ActivityCard/index.tsx index 1d0f9a671..b465a636b 100644 --- a/src/components/activities/ActivityCard/index.tsx +++ b/src/components/activities/ActivityCard/index.tsx @@ -9,9 +9,9 @@ import { AnthemCard } from './AnthemCard'; import { DefiCard } from './DefiCard'; import { EnigmeCard } from './EnigmeCard'; import { FreeContentCard } from './FreeContentCard'; +import { GameCard } from './GameCard'; import { IndiceCard } from './IndiceCard'; import { MascotteCard } from './MascotteCard'; -import { MimicCard } from './MimicCard'; import { PresentationCard } from './PresentationCard'; import { QuestionCard } from './QuestionCard'; import { ReactionCard } from './ReactionCard'; @@ -43,7 +43,7 @@ const CardTypeMapper = { [ActivityType.CONTENU_LIBRE]: FreeContentCard, [ActivityType.INDICE]: IndiceCard, [ActivityType.SYMBOL]: SymbolCard, - [ActivityType.GAME]: MimicCard, + [ActivityType.GAME]: GameCard, [ActivityType.REPORTAGE]: ReportageCard, [ActivityType.REACTION]: ReactionCard, [ActivityType.STORY]: StoryCard, @@ -71,7 +71,6 @@ export const ActivityCard = ({ const userIsPelico = user.type <= UserType.MEDIATOR; const ActivityIcon = icons[activity.type] || null; const timeLeft = isEnigme(activity) ? getEnigmeTimeLeft(activity) : 0; - const UsedCard = CardTypeMapper[activity.type]; return ( @@ -102,6 +101,7 @@ export const ActivityCard = ({ displayAsUser={activity.displayAsUser} /> )} +

+ + {!showEditButtons && isEnigme(activity) && ( + <> + +
+ {timeLeft > 0 ? `Temps restant: ${timeLeft}j` : 'La réponse est disponible !'} +
+ + )} {activity.isPinned && } {ActivityIcon && !isReaction(activity) && ( > | null; disabled?: boolean; + onClick?: () => void; } -export const ActivityChoiceButton = ({ label, href, icon: Icon = null, disabled = false }: ActivityChoiceButtonProps) => { +export const ActivityChoiceButton = ({ onClick, label, href, icon: Icon = null, disabled = false }: ActivityChoiceButtonProps) => { return ( {disabled ? ( @@ -35,7 +36,7 @@ export const ActivityChoiceButton = ({ label, href, icon: Icon = null, disabled {label} ) : ( - + > | null; disabled: boolean; disabledText: string; + gameType?: GameType; }>; } export const ActivityChoice = ({ activities }: ActivityChoiceProps) => { + const { setGameType } = useContext(GameContext); + + const handleChoiceClick = useCallback( + (gameType: GameType) => { + localStorage.setItem('gameTypeInLocalStorage', JSON.stringify(gameType)); + setGameType(gameType); + }, + [setGameType], + ); return (
{ href={activity.href} icon={activity.icon} disabled={activity.disabled} + onClick={() => activity.gameType && handleChoiceClick(activity.gameType)} /> ); })} diff --git a/src/components/activities/GameStats.tsx b/src/components/activities/GameStats.tsx index 9bff9d612..9a590cc64 100644 --- a/src/components/activities/GameStats.tsx +++ b/src/components/activities/GameStats.tsx @@ -48,36 +48,32 @@ const GameStats = ({ gameResponses, choices, country, userMap, users, position } {responseCount > 0 && choices && - choices.map((choice, index) => ( - <> - {responsesByChoice[choice] ? ( -
- {responsesByChoice[choice]?.map((response) => { - const user = users[userMap[response.userId]]; - const renderAvatar = - user.type >= UserType.MEDIATOR ? ( -
- -
- ) : null; - return renderAvatar; - })} -
- ) : ( -
- )} - - ))} + choices.map((choice, index) => + responsesByChoice[choice] ? ( +
+ {responsesByChoice[choice]?.map((response) => + users[userMap[response.userId]].type >= UserType.MEDIATOR ? ( +
+ +
+ ) : null, + )} +
+ ) : ( +
+ ), + )} ); }; diff --git a/src/components/activities/List.tsx b/src/components/activities/List.tsx index 6111477cb..02682a9ff 100644 --- a/src/components/activities/List.tsx +++ b/src/components/activities/List.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import type { SelectChangeEvent } from '@mui/material'; -import { Typography, Button } from '@mui/material'; +import { Button } from '@mui/material'; import PaginationNav from '../PaginationNav/PaginationNav'; import { ActivityCard } from './ActivityCard'; @@ -12,7 +12,8 @@ import { useVillageUsers } from 'src/services/useVillageUsers'; import { defaultTextButtonStyle, primaryColor } from 'src/styles/variables.const'; import ArrowRight from 'src/svg/arrow-right.svg'; import ReactionIcon from 'src/svg/navigation/reaction-icon.svg'; -import type { Activity } from 'types/activity.type'; +import type { Activity, AnyData } from 'types/activity.type'; +import type { User } from 'types/user.type'; interface ActivitiesProps { activities: Activity[]; @@ -22,6 +23,33 @@ interface ActivitiesProps { onSelect?: (index: number) => void; } +type CardProps = { + activity: Activity; + user: User | null; + users: User[]; + userMap: { [key: number]: number }; + noButtons: boolean; + onSelect?: (index: number) => void; + index: number; +}; + +const Card = ({ activity, user, users, userMap, noButtons, index, onSelect = undefined }: CardProps) => ( + { + onSelect(index); + } + : undefined + } + /> +); + export const Activities = ({ activities, noButtons = false, withLinks = false, withPagination = false, onSelect }: ActivitiesProps) => { const [{ selectedActivityId, responseActivityId }, setResponseActivityId] = React.useState<{ selectedActivityId: number | null; @@ -60,111 +88,98 @@ export const Activities = ({ activities, noButtons = false, withLinks = false, w const startIdx = (page - 1) * activitiesPerPage; const endIdx = startIdx + activitiesPerPage; - if (activities.length === 0) { - return Aucune activité existante.; - } + const cardProps = { + user, + users, + userMap, + noButtons, + onSelect, + }; + + const currentPageActivities = activities.filter((activity) => !isAnthem(activity)).slice(startIdx, endIdx); return (
- {activities - .filter((activity) => !isAnthem(activity)) - .slice(startIdx, endIdx) - .map((activity, index) => { - const card = ( - { - onSelect(index); - } - : undefined - } - /> - ); - if (withLinks && activity.responseActivityId !== null) { - const translated = selectedActivityId === activity.id; - return ( -
+ {currentPageActivities.map((activity, index) => + withLinks && activity.responseActivityId !== null ? ( +
+
+
+
+ +
-
-
{card}
-
{ + setResponseActivityId({ selectedActivityId: activity.id, responseActivityId: activity.responseActivityId }); + }} + style={{ display: 'block', lineHeight: '0.8rem', padding: '0.4rem 0 0 0', margin: '0' }} + > + + En réaction à + + - -
-
-
-
- -
+ /> + +
+
+
+
+ +
-
- {responseActivity !== null && ( - - )} -
-
+
+ {responseActivity !== null && ( + + )}
- ); - } - return card; - })} +
+
+ ) : ( + + ), + )} {withPagination && ( Promise; + onClick: (value: number, isSuccess?: boolean) => Promise; isCorrect?: boolean; mimicOrigine?: string; }; @@ -22,8 +21,8 @@ const ResponseButton = ({ signification = '', disabled = false, isCorrect, - mimicOrigine, -}: ResponseButtonProps) => { +}: // mimicOrigine, +ResponseButtonProps) => { const [hasBeenSelected, setHasBeenSelected] = useState(false); const handleClick = useCallback(() => { @@ -57,7 +56,7 @@ const ResponseButton = ({ {isCorrect ? ( {signification} - Origine : {mimicOrigine || ''} + {/* Origine : {mimicOrigine || ''} */} ) : ( signification diff --git a/src/components/game/CreateGame.tsx b/src/components/game/CreateGame.tsx new file mode 100644 index 000000000..2c61a5e91 --- /dev/null +++ b/src/components/game/CreateGame.tsx @@ -0,0 +1,66 @@ +import React, { useContext } from 'react'; + +import GameField from './componentGameMapping/GameField'; +import GameMedia from './componentGameMapping/GameMedia'; +import GameRadio from './componentGameMapping/GameRadio'; +import GameSelect from './componentGameMapping/GameSelect'; +import { GameContext } from 'src/contexts/gameContext'; +import type { hiddenType, inputType } from 'types/game.type'; +import { InputTypeEnum } from 'types/game.type'; + +interface PlayProps { + stepNumber: number; +} + +const ComponentMapping = { + [InputTypeEnum.SELECT]: GameSelect, + [InputTypeEnum.RADIO]: GameRadio, + [InputTypeEnum.INPUT]: GameField, + [InputTypeEnum.IMAGE]: GameMedia, + [InputTypeEnum.VIDEO]: GameMedia, +}; + +const CreateGame = ({ stepNumber }: PlayProps) => { + const { gameConfig } = useContext(GameContext); + + if (!gameConfig || !gameConfig[stepNumber]) { + return
Oups, votre jeu n'existe pas encore
; + } + + const checkDisPlayCondition = (hidden: hiddenType) => { + let inputToCompare: inputType = {} as inputType; + gameConfig.map((page) => + page.map((step) => + step.inputs?.map((input) => { + if (input.id === hidden.id) { + inputToCompare = input; + } + }), + ), + ); + if (inputToCompare?.selectedValue === hidden.value) return false; + return true; + }; + + return ( + <> + {gameConfig[stepNumber].map((stepItem, index) => ( +
+

{stepItem.title}

+
+

+ {stepItem.description} +

+ {stepItem.inputs?.map((input, inputIndex) => { + const Component = ComponentMapping[input.type]; + const isDisplayed = !input.hidden || checkDisPlayCondition(input.hidden); + return isDisplayed ? : null; + })} +
+
+ ))} + + ); +}; + +export default CreateGame; diff --git a/src/components/game/DisplayGameById.tsx b/src/components/game/DisplayGameById.tsx new file mode 100644 index 000000000..f9a7cf5fb --- /dev/null +++ b/src/components/game/DisplayGameById.tsx @@ -0,0 +1,534 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React, { useState, useCallback, useMemo, useContext } from 'react'; + +// import AccessTimeIcon from '@mui/icons-material/AccessTime'; +// import ShuffleIcon from '@mui/icons-material/Shuffle'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { Box, Button, Grid } from '@mui/material'; + +import { KeepRatio } from '../KeepRatio'; +import { useOneGameById } from 'src/api/game/game.getOneGameById'; +import { AvatarImg } from 'src/components/Avatar'; +import { Base } from 'src/components/Base'; +import { Flag } from 'src/components/Flag'; +import { Modal } from 'src/components/Modal'; +import { UserDisplayName } from 'src/components/UserDisplayName'; +import { RightNavigation } from 'src/components/accueil/RightNavigation'; +import GameStats from 'src/components/activities/GameStats'; +import { VideoView } from 'src/components/activities/content/views/VideoView'; +import ResponseButton from 'src/components/buttons/GameResponseButton'; +import { UserContext } from 'src/contexts/userContext'; +import { VillageContext } from 'src/contexts/villageContext'; +import { useGameRequests } from 'src/services/useGames'; +import { useVillageUsers } from 'src/services/useVillageUsers'; +import { primaryColor } from 'src/styles/variables.const'; +import PelicoNeutre from 'src/svg/pelico/pelico_neutre.svg'; +import { GameType } from 'types/game.type'; +import type { Game } from 'types/game.type'; +import type { GameResponse } from 'types/gameResponse.type'; +import { UserType } from 'types/user.type'; + +function shuffleArray(size: number) { + const array = Array.from(Array(size)).map((_, i) => i); + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + return array; +} + +/* partie à déplacer et à centraliser avec les prochains jeux*/ +type AlreadyPlayedModalProps = { + isOpen: boolean; + gameId: number; + handleSuccessClick: () => void; +}; + +enum RadioBoxValues { + NEW = 'Nouvelle', + RANDOM = 'Aléatoire', + // MOSAIC = 'Mosaïque', +} + +// type RadioNextGameProps = { +// value: RadioBoxValues; +// Icon: OverridableComponent> & { +// muiName: string; +// }; +// onChange: (event: React.SyntheticEvent) => void; +// checked: boolean; +// }; + +// const radioListComponentMapper = { +// [RadioBoxValues.NEW]: AccessTimeIcon, +// [RadioBoxValues.RANDOM]: ShuffleIcon, +// //[RadioBoxValues.MOSAIC]: AppsIcon, +// }; + +// const RadioNextGame: React.FC = ({ value, Icon, onChange, checked }) => ( +// } +// label={ +// <> +// {value} +// +// } +// labelPlacement="end" +// checked={checked} +// onChange={onChange} +// /> +// ); + +const AlreadyPlayedModal: React.FC = ({ isOpen, handleSuccessClick }) => { + const router = useRouter(); + + return ( + router.push('/')} + onConfirm={handleSuccessClick} + > + C’était le dernier jeu disponible ! Dès que de nouveaux seront ajoutées, cela apparaîtra dans le fil d’activité. + + ); +}; +/* FIN partie à déplacer et à centraliser avec les prochains jeux*/ + +const POSITION = ['c', 'f', 'i']; + +type SubTypeProps = { + subType: GameType; +}; + +const DisplayGameById = ({ subType }: SubTypeProps) => { + const { user } = useContext(UserContext); + const { village } = useContext(VillageContext); + const { users } = useVillageUsers(); + const { getRandomGame, sendNewGameResponse, getGameStats, getAvailableGames, resetGamesPlayedForUser } = useGameRequests(); + const [tryCount, setTryCount] = useState(0); + const [found, setFound] = useState(false); + const [errorModalOpen, setErrorModalOpen] = useState(false); + const [isGameModalOpen, setIsLastGameModalOpen] = useState(false); + const [gameResponses, setGameResponses] = useState([]); + const [selectedValue] = useState(RadioBoxValues.NEW); + const router = useRouter(); + const { id } = router.query; + + const gameId = parseInt(String(id)); + const { data: getOneGameById } = useOneGameById(subType, gameId || 0); + + const TYPE_OF_GAME = { + [GameType.MIMIC]: 'mimique', + [GameType.MONEY]: 'objet', + [GameType.EXPRESSION]: 'expression', + }; + + const phrase = { + [GameType.MIMIC]: { + title: 'des mimiques', + phraseDelamodal: 'la signification de cette mimique', + question: 'Que signifie cette mimique ?', + presentation: 'Une mimique proposé par ', + moreInfos: '', + }, + [GameType.MONEY]: { + title: 'de la monnaie', + phraseDelamodal: 'combien vaut cet objet', + question: 'Combien vaut cet objet ?', + presentation: 'Un objet proposé par ', + moreInfos: '', + }, + [GameType.EXPRESSION]: { + title: 'des expressions', + phraseDelamodal: 'la signification de cette expression', + question: 'Que signifie cette expression ?', + presentation: 'Une expression proposé par ', + moreInfos: ` en ${getOneGameById?.content.language?.toLowerCase()}, ${getOneGameById?.content.radio}`, + }, + }; + + const typeOfGame = TYPE_OF_GAME[subType]; + const displayPhrasesByType = phrase[subType]; + const handleConfirmModal = async () => { + const success = await resetGamesPlayedForUser(); + setIsLastGameModalOpen(false); + + if (success) { + router.reload(); + } else { + router.push('/'); + } + }; + const sortGamesByDescOrder = (games: Game[]) => { + return games.sort((a, b) => { + const dateA = a.createDate ? new Date(a.createDate).getTime() : 0; + const dateB = b.createDate ? new Date(b.createDate).getTime() : 0; + return dateB - dateA; + }); + }; + + const getNextGame = useCallback(async () => { + // [1] Reset game. + setFound(false); + setGameResponses([]); + setTryCount(0); + setErrorModalOpen(false); + + const allGames = await getAvailableGames(subType); + const availableGamesByDescOrder = sortGamesByDescOrder(allGames); + const currentGameIndex = availableGamesByDescOrder.findIndex((g) => g.id === gameId); + const availableGames = allGames.filter((g) => g.id !== gameId); + + const isLastGame = availableGames.length === 0; + + const NEXT_GAME_MAPPER = { + [RadioBoxValues.NEW]: () => availableGamesByDescOrder[currentGameIndex + 1 < availableGamesByDescOrder.length ? currentGameIndex + 1 : 0], + [RadioBoxValues.RANDOM]: async () => { + return await getRandomGame(subType); + }, + }; + + const nextGame = isLastGame ? undefined : await NEXT_GAME_MAPPER[selectedValue](); + + if (isLastGame || !nextGame) { + setIsLastGameModalOpen(true); + return; + } + router.push(`./${nextGame?.id}`); + }, [getAvailableGames, subType, selectedValue, router, gameId, getRandomGame]); + + const userMap = useMemo( + () => + users.reduce<{ [key: number]: number }>((acc, u, index) => { + acc[u.id] = index; + return acc; + }, {}), + [users], + ); + + const playContent = useMemo(() => { + if (!getOneGameById) { + return { responses: [] }; + } + + const { + id, + content, + content: { + game: steps, + labelPresentation, + radio, + language, + monney, + game: [ + { + inputs: [{ selectedValue: media, type }], + }, + ], + }, + createDate, + villageId, + } = getOneGameById || {}; + + const responses: { signification: string; isSuccess: boolean; value: number }[] = []; + + let fakeSignificationIndex = 1; + const euro = content.monney; + steps.map(({ inputs }) => { + inputs.map((input) => { + if (input.response || input.response === false) { + if (getOneGameById.subType === GameType.MONEY) { + // Utilisation des templates de chaînes de caractères + const significationWithEuro = `${input.selectedValue} ${euro}`; + responses.push({ isSuccess: input.response, signification: significationWithEuro, value: input.response ? 0 : fakeSignificationIndex }); + } else { + responses.push({ isSuccess: input.response, signification: input.selectedValue, value: input.response ? 0 : fakeSignificationIndex }); + } + if (input.response === false) { + ++fakeSignificationIndex; + } + } + }); + }); + return { + responses, + labelPresentation, + radio, + monney, + language, + createDate, + id, + media, + type, + villageId, + }; + }, [getOneGameById]); + + const gameCreator = useMemo(() => { + if (getOneGameById === undefined) { + return undefined; + } + return userMap[getOneGameById.userId] !== undefined ? users[userMap[getOneGameById.userId]] : undefined; + }, [getOneGameById, userMap, users]); + const gameCreatorIsPelico = gameCreator !== undefined && gameCreator.type <= UserType.MEDIATOR; + const userIsPelico = user !== null && user.type <= UserType.MEDIATOR; + + const choices = React.useMemo(() => (playContent.responses.length > 0 ? shuffleArray(playContent.responses.length) : []), [playContent.responses]); + + // const handleRadioButtonChange = (event: React.SyntheticEvent) => { + // const selected = (event as React.ChangeEvent).target.value; + // setSelectedValue(selected as RadioBoxValues); + // if (selected === RadioBoxValues.RANDOM) { + // getNextGame(); + // } + // }; + + const handleClick = useCallback( + async (selection: string, isSuccess: boolean = false) => { + if (playContent.responses.length === 0) { + return; + } + const apiResponse = await sendNewGameResponse(playContent.id || 0, selection, playContent.villageId || 0); + if (!apiResponse) { + console.error('Error reaching server'); + return; + } + + setFound(isSuccess); + setErrorModalOpen(!isSuccess); + if (isSuccess || tryCount === 1) { + setGameResponses(await getGameStats(playContent.id || 0)); + } + setTryCount(tryCount + 1); + }, + [ + getGameStats, + sendNewGameResponse, + setFound, + playContent.responses, + setErrorModalOpen, + setGameResponses, + setTryCount, + tryCount, + playContent?.id, + playContent.villageId, + ], + ); + + if (user == null || village == null) { + return ; + } + + // Modal dernier jeux + if (playContent.responses.length === 0 || !gameCreator) { + return ( + + + + ); + } + + return ( + } hideLeftNav showSubHeader> +
+ + +
+ {displayPhrasesByType && ( +
+

Jouer au jeu {displayPhrasesByType.title} !

+
+ )} +
+
+
+ +
+

+ {displayPhrasesByType.presentation} + + {displayPhrasesByType.moreInfos} +

+ {playContent && playContent.createDate && ( +

+ {'Publié le '} + {new Date(playContent.createDate).toLocaleDateString()} + {gameCreatorIsPelico ? ( + + ) : ( + gameCreator && gameCreator.country && + )} +

+ )} +
+
+ + {/* + {Object.keys(radioListComponentMapper).map((value: string, index: number) => { + return ( + + ); + })} + */} + + + + + + + + {playContent !== undefined && playContent.media !== null && playContent.type === 4 && ( + + )} + {playContent !== undefined && playContent.media !== null && playContent.type === 3 && ( +
+ + + +
+ )} +
+ +

{displayPhrasesByType.question}

+
+
+ {choices && + choices.map((val, index) => { + const { value, isSuccess, signification } = playContent.responses[val]; + const isCorrect = isSuccess && found; + const isDisabled = (isSuccess && tryCount > 1) || (!isSuccess && found); + return ( +
+ handleClick(value.toString(), isSuccess)} + isSuccess={isSuccess} + signification={signification} + disabled={isDisabled} + isCorrect={isCorrect || (tryCount > 1 && isSuccess)} + // mimicOrigine={mimicOrigine} + /> +
+ ); + })} + {(found || tryCount > 1) && ( + <> + {user.country && ( + + )} + c.isoCode).find((i) => i !== user.country?.isoCode) || '' + } + userMap={userMap} + users={users} + /> + + )} +
+ + + {found &&

C’est exact ! Vous avez trouvé {displayPhrasesByType.phraseDelamodal}.

} +
+
+ {/* */} +
+ 1 && !found ? 'Fermer' : 'Réessayer'} + maxWidth="lg" + ariaDescribedBy="new-user-desc" + ariaLabelledBy="new-user-title" + onClose={() => setErrorModalOpen(false)} + > + {tryCount > 1 && !found ? ( +

Dommage ! Vous n’avez pas trouvé la bonne réponse cette fois-ci.

+ ) : ( +

Dommage ! Ce n’est pas cette réponse. Essayez encore !

+ )} +
+ + + + {(found || tryCount > 1) && ( +
+

+ + {"Revenir à l'accueil"} + +

+
+ )} +
+ + + +
+
+ + ); +}; + +export default DisplayGameById; diff --git a/src/components/game/List.tsx b/src/components/game/List.tsx new file mode 100644 index 000000000..d5c22cfa1 --- /dev/null +++ b/src/components/game/List.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import ReactPlayer from 'react-player'; + +import { Grid, Link } from '@mui/material'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardMedia from '@mui/material/CardMedia'; +import ImageListItem from '@mui/material/ImageListItem'; +import ImageListItemBar from '@mui/material/ImageListItemBar'; + +import { useAllStandardGameByType } from 'src/api/game/game.getAllBySubtype'; +import { useAbleToPlayStandardGame } from 'src/api/game/game.getAvailable'; +import { primaryColor } from 'src/styles/variables.const'; +import { GameType } from 'types/game.type'; + +// This mapping is used to create routes dynamically +const TYPE_OF_GAME = { + [GameType.MIMIC]: 'mimique', + [GameType.MONEY]: 'objet', + [GameType.EXPRESSION]: 'expression', +}; + +type SubTypeProps = { + subType: GameType; + villageId: number | undefined; +}; + +const List = ({ subType, villageId }: SubTypeProps) => { + const typeOfGame = TYPE_OF_GAME[subType]; + + const { data: allGames } = useAllStandardGameByType(subType, villageId !== undefined ? villageId : 0); + const { data: ableToPlay } = useAbleToPlayStandardGame(subType, villageId !== undefined ? villageId : 0); + const idFromAbleToPlay: number[] = []; + + ableToPlay?.map((el: { id: number }) => { + idFromAbleToPlay.push(el.id); + }); + + return ( + <> + + {allGames && allGames.length > 0 ? ( + allGames.map( + ( + item: { + id: number; + content: { + game: { inputs: { selectedValue: string | undefined }[] }[]; + labelPresentation: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined; + }; + }, + index: number, + ) => ( + + + + + {subType === 0 ? ( +
+ +
+ ) : ( + + )} + +
+
+ +
+
+ + {idFromAbleToPlay.includes(item.id) ? ( + + ) : ( + + )} + +
+ ), + ) + ) : ( +
Oups, on dirait qu'il n'y a aucun jeu pour le moment.
+ )} +
+ + ); +}; +export default List; diff --git a/src/components/game/Play.tsx b/src/components/game/Play.tsx new file mode 100644 index 000000000..842bea521 --- /dev/null +++ b/src/components/game/Play.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +// import { Grid } from '@mui/material'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import InputLabel from '@mui/material/InputLabel'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Select from '@mui/material/Select'; +import TextField from '@mui/material/TextField'; + +import { GAME_FIELDS_CONFIG } from 'src/config/games/game'; +import { InputTypeEnum } from 'types/game.type'; +import type { GameType } from 'types/game.type'; + +interface PlayProps { + gameType: GameType; + stepNumber: number; +} + +const Play: React.FC = ({ gameType, stepNumber }) => { + const gameConfig = GAME_FIELDS_CONFIG[gameType]; + + if (!gameConfig || !gameConfig.steps[stepNumber]) { + return
Configuration introuvable
; + } + + return ( + <> + {gameConfig.steps[stepNumber].map((stepItem, index) => ( + //début Div 1 +
+

{stepItem.title}

+ + {/* début div 2 */} +
+

{stepItem.description}

+ {stepItem.inputs && + stepItem.inputs.map((input, inputIndex) => ( +
+ {input.label && {input.label}} + {input.type === InputTypeEnum.SELECT && ( + + + + )} + {input.type === InputTypeEnum.RADIO && ( + + {input.values && + input.values.map((radioValue, radioIndex) => ( + } label={radioValue} /> + ))} + + )} + {input.type === InputTypeEnum.INPUT && } +
+ ))} + + {/* fin Div 2 */} +
+ + {/* fin Div 1 */} +
+ ))} + + ); +}; + +export default Play; diff --git a/src/components/game/Previsualisation.tsx b/src/components/game/Previsualisation.tsx new file mode 100644 index 000000000..a855afce2 --- /dev/null +++ b/src/components/game/Previsualisation.tsx @@ -0,0 +1,110 @@ +import Image from 'next/image'; +import router from 'next/router'; +import React, { useContext } from 'react'; +import ReactPlayer from 'react-player'; + +import { FormControlLabel, Grid, Radio, RadioGroup } from '@mui/material'; + +import { CustomRadio } from '../buttons/CustomRadio'; +import { EditButton } from '../buttons/EditButton'; +import { GameContext } from 'src/contexts/gameContext'; +import type { inputType } from 'types/game.type'; +import { InputTypeEnum } from 'types/game.type'; + +type PrevisualisationProps = { + baseUrl: string; +}; + +const Previsualisation = ({ baseUrl }: PrevisualisationProps) => { + const { gameConfig } = useContext(GameContext); + const result = gameConfig + .map((step, index) => { + const reponseInStep: inputType[][] = step.map((responseBlock) => { + const filteredItems = responseBlock?.inputs?.filter((input) => { + return input.isDisplayedInRecap === true; + }); + return filteredItems || []; + }); + return { + step: index + 1, + responses: ([] as inputType[]).concat(...reponseInStep), + }; + }) + .filter((elem) => elem.responses.length > 0); + + return ( +
+ <> + {result.map((step, stepIndex) => { + const AArea = step.responses.filter((res) => res?.type === InputTypeEnum.IMAGE || res?.type === InputTypeEnum.VIDEO); + const BArea = step.responses.filter((res) => res?.response === true || res?.response === false); + const CArea = step.responses.filter( + (res) => res?.type !== InputTypeEnum.IMAGE && res?.type !== InputTypeEnum.VIDEO && res?.response !== true && res?.response !== false, + ); + const hasCArea = CArea.length >= 1; + return ( +
+
+ { + router.push(`${baseUrl}${step.step}`); + }} + status="success" + /> +
+
+ {AArea.map((elem, index) => ( +
+ {/* Taille Image a régler + Liens en fonction de la steps incrémentation + Boutton */} + {elem.selectedValue && elem.type === 3 ? ( + Image à deviner + ) : elem.selectedValue && elem.type === 4 ? ( + + ) : null} +
+ ))} +
+
+ + + {BArea.map((elem, index) => ( + : } + label={elem.selectedValue} + style={{ maxWidth: '100%' }} + /> + ))} + + +
+ {hasCArea && ( +
+ {CArea.map((elem, index) => ( +
+ {elem?.selectedValue} +
+ ))} +
+ )} +
+ ); + })} + +
+ ); +}; + +export default Previsualisation; diff --git a/src/components/game/componentGameMapping/GameField.tsx b/src/components/game/componentGameMapping/GameField.tsx new file mode 100644 index 000000000..9e8c62581 --- /dev/null +++ b/src/components/game/componentGameMapping/GameField.tsx @@ -0,0 +1,35 @@ +import React, { useContext, useState } from 'react'; + +import { TextField } from '@mui/material'; + +import { GameContext } from 'src/contexts/gameContext'; +import type { inputType } from 'types/game.type'; + +const GameField = ({ input }: { input: inputType }) => { + const { updateGameConfig } = useContext(GameContext); + const [value, setValue] = useState(''); + + const handleChange = (event: React.SyntheticEvent) => { + const v = (event.target as HTMLInputElement).value; + setValue(v); + updateGameConfig(v, input); + }; + + return ( + <> +

+ {input.label} +

+ 0 ? input.selectedValue : value} + placeholder={value.length > 0 ? '' : input.placeHolder} + onChange={handleChange} + required + /> + + ); +}; + +export default GameField; diff --git a/src/components/game/componentGameMapping/GameMedia.tsx b/src/components/game/componentGameMapping/GameMedia.tsx new file mode 100644 index 000000000..c9853a633 --- /dev/null +++ b/src/components/game/componentGameMapping/GameMedia.tsx @@ -0,0 +1,143 @@ +import Image from 'next/image'; +import React, { useContext } from 'react'; +import ReactPlayer from 'react-player'; + +import AddIcon from '@mui/icons-material/Add'; +import { ButtonBase } from '@mui/material'; + +import { KeepRatio } from 'src/components/KeepRatio'; +import { ImageModal } from 'src/components/activities/content/editors/ImageEditor/ImageModal'; +import { VideoModals } from 'src/components/activities/content/editors/VideoEditor/VideoModals'; +import { DeleteButton } from 'src/components/buttons/DeleteButton'; +import { GameContext } from 'src/contexts/gameContext'; +import { errorColor, primaryColor, bgPage } from 'src/styles/variables.const'; +import type { inputType } from 'types/game.type'; + +const GameMedia = ({ input }: { input: inputType }) => { + const { updateGameConfig } = useContext(GameContext); + const [isError, setIsError] = React.useState(false); + const [isImageModalOpen, setIsImageModalOpen] = React.useState(false); + + const handleChange = (value: string) => { + updateGameConfig(value, input); + }; + + const setImage = (value: string) => { + updateGameConfig(value, input); + }; + + const handleImageError = () => { + setIsError(true); + }; + + return ( + <> + {input.type == 3 && ( +
+
+ setIsImageModalOpen(true)} style={{ width: '100%', color: `${isError ? errorColor : primaryColor}` }}> + +
+ {input.selectedValue ? ( + <> + Image à deviner + {isError ? ( +

Oups un problème est survenue. Veuillez vérifiez votre url ou le format d'image.

+ ) : null} + + ) : ( + + )} +
+
+
+ {input.selectedValue && ( +
+ { + setIsError(false); + setImage(''); + }} + confirmLabel="Êtes-vous sur de vouloir supprimer l'image ?" + confirmTitle="Supprimer l'image" + style={{ backgroundColor: bgPage }} + /> +
+ )} + +
+
+ )} + {input.type == 4 && ( +
+
+ setIsImageModalOpen(true)} style={{ width: '100%', color: `${isError ? errorColor : primaryColor}` }}> + +
+ {input.selectedValue ? ( + <> + + {isError ? ( +

Oups un problème est survenue. Veuillez vérifiez votre url ou le format de vidéo.

+ ) : null} + + ) : ( + + )} +
+
+
+ {input.selectedValue && ( +
+ { + setIsError(false); + setImage(''); + }} + confirmLabel="Êtes-vous sur de vouloir supprimer la vidéo ?" + confirmTitle="Supprimer la vidéo" + style={{ backgroundColor: bgPage }} + /> +
+ )} + +
+
+ )} + + ); +}; + +export default GameMedia; diff --git a/src/components/game/componentGameMapping/GameRadio.tsx b/src/components/game/componentGameMapping/GameRadio.tsx new file mode 100644 index 000000000..d6bb12400 --- /dev/null +++ b/src/components/game/componentGameMapping/GameRadio.tsx @@ -0,0 +1,49 @@ +import React, { useContext } from 'react'; + +import { RadioGroup, Radio, FormControlLabel } from '@mui/material'; + +import { GameContext } from 'src/contexts/gameContext'; +import type { inputType } from 'types/game.type'; + +const GameRadio = ({ input }: { input: inputType }) => { + const { updateGameConfig } = useContext(GameContext); + + const handleChange = (event: React.SyntheticEvent) => { + const value = (event.target as HTMLInputElement).value; + updateGameConfig(value, input); + }; + + return ( + <> + {input.selectedValue && input.selectedValue.length > 0 ? ( + + {input.values?.map((radioValue: string, radioIndex) => ( + } + label={radioValue} + style={{ cursor: 'pointer' }} + onChange={handleChange} + /> + ))} + + ) : ( + + {input.values?.map((radioValue: string, radioIndex) => ( + } + label={radioValue} + style={{ cursor: 'pointer' }} + onChange={handleChange} + /> + ))} + + )} + + ); +}; + +export default GameRadio; diff --git a/src/components/game/componentGameMapping/GameSelect.tsx b/src/components/game/componentGameMapping/GameSelect.tsx new file mode 100644 index 000000000..16f762f6c --- /dev/null +++ b/src/components/game/componentGameMapping/GameSelect.tsx @@ -0,0 +1,63 @@ +import React, { useContext, useEffect, useState } from 'react'; + +import { Autocomplete, FormControl, TextField } from '@mui/material'; + +import { SelectTypeMappingMethode, keyMapping } from 'src/config/games/game'; +import { GameContext } from 'src/contexts/gameContext'; +import type { Currency } from 'types/currency.type'; +import type { inputType } from 'types/game.type'; +import type { Language } from 'types/language.type'; + +const GameSelect = ({ input }: { input: inputType }) => { + const [values, setValues] = useState([]); + const { updateGameConfig } = useContext(GameContext); + + function mapOptionValue(optionValue: Currency | Language, key: string): string { + if (key in optionValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (optionValue as any)[key]; + } + throw new Error(`Key '${key}' not found in optionValue`); + } + + useEffect(() => { + const getValues = async () => { + if (!input.methodType) return; + const res = await SelectTypeMappingMethode[input.methodType](); + const key = keyMapping[input.methodType]; + const optionValues = res.map((optionValue) => mapOptionValue(optionValue, key)); + setValues(optionValues); + }; + getValues(); + }, [input.methodType]); + + return ( + <> + {input.selectedValue && input.selectedValue.length > 0 ? ( + + option} + renderInput={(params) => } + onChange={(_event, newValue) => { + updateGameConfig(newValue, input); + }} + /> + + ) : ( + + option} + renderInput={(params) => } + onChange={(_event, newValue) => { + updateGameConfig(newValue, input); + }} + /> + + )} + + ); +}; + +export default GameSelect; diff --git a/src/config/games/game.ts b/src/config/games/game.ts new file mode 100644 index 000000000..2036a38f9 --- /dev/null +++ b/src/config/games/game.ts @@ -0,0 +1,679 @@ +import { getCurrencies } from 'src/api/currencie/currencies.get'; +import { getLanguages } from 'src/api/language/languages.get'; +import type { GameFieldConfigType } from 'types/game.type'; +import { GameType, InputTypeEnum, methodType } from 'types/game.type'; + +export const keyMapping = { + [methodType.CURRENCY]: 'name', + [methodType.LANGUE]: 'french', +}; + +export const SelectTypeMappingMethode = { + [methodType.CURRENCY]: getCurrencies, + [methodType.LANGUE]: getLanguages, +}; + +export const GAME_FIELDS_CONFIG: GameFieldConfigType = { + [GameType.MIMIC]: { + steps: [ + [ + { + title: 'Présentez en vidéo une 1ère mimique à vos Pélicopains', + description: + 'Votre vidéo est un plan unique tourné à l’horizontal, qui montre un élève faisant la mimique et la situation dans laquelle on l’utilise.. Gardez le mystère, et ne révélez pas à l’oral sa signification !', + inputs: [ + { + id: 0, + type: InputTypeEnum.VIDEO, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 1, + type: InputTypeEnum.INPUT, + label: 'Que signifie cette mimique ?', + placeHolder: 'Signification réelle', + isDisplayedInRecap: true, + response: true, + required: true, + selectedValue: '', + }, + { + id: 2, + type: InputTypeEnum.INPUT, + label: 'Quelle est l’origine de cette mimique ?', + placeHolder: 'Origine', + isDisplayedInRecap: true, + selectedValue: '', + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette mimique', + description: + 'Vos Pélicopains verront la vidéo de votre mimique, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 3, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 1', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + { + id: 4, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 2', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + ], + }, + ], + [ + { + title: 'Présentez en vidéo une 2ème mimique à vos Pélicopains', + description: + 'Votre vidéo est un plan unique tourné à l’horizontal, qui montre un élève faisant la mimique et la situation dans laquelle on l’utilise.. Gardez le mystère, et ne révélez pas à l’oral sa signification !', + inputs: [ + { + id: 5, + type: InputTypeEnum.VIDEO, + selectedValue: '', + required: true, + isDisplayedInRecap: true, + }, + { + id: 6, + type: InputTypeEnum.INPUT, + label: 'Que signifie cette mimique ?', + placeHolder: 'Signification réelle', + required: true, + isDisplayedInRecap: true, + response: true, + selectedValue: '', + }, + { + id: 7, + type: InputTypeEnum.INPUT, + label: 'Quelle est l’origine de cette mimique ?', + placeHolder: 'Origine', + isDisplayedInRecap: true, + selectedValue: '', + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette mimique', + description: + 'Vos Pélicopains verront la vidéo de votre mimique, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 8, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 1', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + { + id: 9, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 2', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + ], + }, + ], + [ + { + title: 'Présentez en vidéo une 3ème mimique à vos Pélicopains', + description: + 'Votre vidéo est un plan unique tourné à l’horizontal, qui montre un élève faisant la mimique et la situation dans laquelle on l’utilise.. Gardez le mystère, et ne révélez pas à l’oral sa signification !', + inputs: [ + { + id: 10, + type: InputTypeEnum.VIDEO, + selectedValue: '', + required: true, + isDisplayedInRecap: true, + }, + { + id: 11, + type: InputTypeEnum.INPUT, + label: 'Que signifie cette mimique ?', + placeHolder: 'Signification réelle', + required: true, + isDisplayedInRecap: true, + response: true, + selectedValue: '', + }, + { + id: 12, + type: InputTypeEnum.INPUT, + label: 'Quelle est l’origine de cette mimique ?', + placeHolder: 'Origine', + isDisplayedInRecap: true, + selectedValue: '', + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette mimique', + description: + 'Vos Pélicopains verront la vidéo de votre mimique, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 13, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 1', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + { + id: 14, + type: InputTypeEnum.INPUT, + placeHolder: 'Signification inventée 2', + required: true, + isDisplayedInRecap: true, + response: false, + selectedValue: '', + }, + ], + }, + ], + [ + { + title: 'Pré-visualisez votre activité et publiez-la.', + description: + 'Voici la pré-visualisation de votre activité. Vous pouvez la modifier, et quand vous êtes prêts : publiez-la dans votre village-monde !', + }, + ], + ], + }, + + [GameType.MONEY]: { + steps: [ + [ + { + title: 'Choisissez votre monnaie', + description: 'Choisissez avec quelle monnaie vous allez donner le prix de vos objets : ', + inputs: [ + { + id: 0, + type: InputTypeEnum.SELECT, + placeHolder: 'Monnaie', + methodType: methodType.CURRENCY, + values: [], + selectedValue: '', + required: true, + }, + ], + }, + ], + [ + { + title: 'Choisissez un objet', + description: 'Choissisez un objet dont le prix moyen est faible', + inputs: [ + { + id: 1, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 2, + type: InputTypeEnum.INPUT, + label: 'Quel est le nom de cet objet ?', + selectedValue: '', + required: true, + }, + { + id: 3, + type: InputTypeEnum.INPUT, + label: 'Quel est son prix moyen en euro ? (Écrire la valeur en nombre)', + selectedValue: '', + response: true, + isDisplayedInRecap: true, + required: true, + }, + { + id: 4, + type: InputTypeEnum.INPUT, + label: 'À quoi sert cet objet ? Quand est-il acheté ?', + selectedValue: '', + required: true, + isIndice: true, + }, + ], + }, + { + title: 'Inventez deux prix faux à cet objet', + description: + 'Vos Pélicopains verront l’image de votre objet, et devront trouver son prix parmi le vrai, et les deux faux, qu’il faut inventer :', + inputs: [ + { + id: 5, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 6, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + [ + { + title: 'Choisissez un objet', + description: 'Choissisez un objet dont le prix moyen est modéré', + inputs: [ + { + id: 7, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 8, + type: InputTypeEnum.INPUT, + label: 'Quel est le nom de cet objet ?', + selectedValue: '', + required: true, + }, + { + id: 9, + type: InputTypeEnum.INPUT, + label: 'Quel est son prix moyen en euro ? (Écrire la valeur en nombre)', + selectedValue: '', + response: true, + isDisplayedInRecap: true, + required: true, + }, + { + id: 10, + type: InputTypeEnum.INPUT, + label: 'À quoi sert cet objet ? Quand est-il acheté ?', + selectedValue: '', + required: true, + isIndice: true, + }, + ], + }, + { + title: 'Inventez deux prix faux à cet objet', + description: + 'Vos Pélicopains verront l’image de votre objet, et devront trouver son prix parmi le vrai, et les deux faux, qu’il faut inventer :', + inputs: [ + { + id: 11, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé 1', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 12, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé 2', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + [ + { + title: 'Choisissez un objet', + description: 'Choissisez un objet dont le prix moyen est élevé', + inputs: [ + { + id: 13, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 14, + type: InputTypeEnum.INPUT, + label: 'Quel est le nom de cet objet ?', + selectedValue: '', + required: true, + }, + { + id: 15, + type: InputTypeEnum.INPUT, + label: 'Quel est son prix moyen en euro ? (Écrire la valeur en nombre)', + selectedValue: '', + response: true, + isDisplayedInRecap: true, + required: true, + }, + { + id: 16, + type: InputTypeEnum.INPUT, + label: 'À quoi sert cet objet ? Quand est-il acheté ?', + selectedValue: '', + required: true, + isIndice: true, + }, + ], + }, + { + title: 'Inventez deux prix faux à cet objet', + description: + 'Vos Pélicopains verront l’image de votre objet, et devront trouver son prix parmi le vrai, et les deux faux, qu’il faut inventer :', + inputs: [ + { + id: 17, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé 1', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 18, + type: InputTypeEnum.INPUT, + placeHolder: 'Prix inventé 2', + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + [ + { + title: 'Pré-visualisez vos objets et publiez les !', + description: 'Vous pouvez modifier chaque objet si vous le souhaitez. Quand vous êtes prêts : publiez-les dans votre village-monde ! ', + }, + ], + ], + }, + + [GameType.EXPRESSION]: { + steps: [ + [ + { + title: 'Choisissez dans quelle langue vous souhaitez lancer le défi', + description: 'Vous pourrez ensuite commencer votre défi', + inputs: [ + { + id: 0, + type: InputTypeEnum.SELECT, + placeHolder: 'Langues', + methodType: methodType.LANGUE, + values: [], + selectedValue: '', + required: true, + }, + ], + }, + { + description: 'Dans votre classe, cette langue est : ', + inputs: [ + { + id: 1, + type: InputTypeEnum.RADIO, + values: [ + 'maternelle chez tous les élèves', + 'maternelle chez certains élèves', + 'utilisée pour faire cours', + 'apprise comme langue étrangère', + ], + selectedValue: '', + required: true, + }, + ], + }, + ], + + [ + { + title: 'Dessinez votre expression', + description: 'Réalisez votre dessin sur une feuille au format paysage', + inputs: [ + { + id: 2, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 3, + label: 'Écrivez l’expression dans la langue que vous avez choisie juste avant', + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + required: true, + }, + { + id: 4, + hidden: { id: 0, value: 'Français' }, + label: 'Écrivez la traduction “mot à mot” en français', + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + }, + { + id: 5, + label: 'Que signifie cette expression ?', + placeHolder: 'Signification réelle', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: true, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette expression', + description: + 'Vos Pélicopains verront le dessin de votre expression, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 6, + placeHolder: 'Signification inventée 1', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 7, + placeHolder: 'Signification inventée 2', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + + [ + { + title: 'Dessinez votre expression', + description: 'Réalisez votre dessin sur une feuille au format paysage', + inputs: [ + { + id: 8, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 9, + label: 'Écrivez l’expression dans la langue que vous avez choisie juste avant', + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + required: true, + }, + { + id: 10, + label: 'Écrivez la traduction “mot à mot” en français', + hidden: { id: 0, value: 'Français' }, + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + }, + { + id: 11, + label: 'Que signifie cette expression ?', + placeHolder: 'Signification réelle', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: true, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette expression', + description: + 'Vos Pélicopains verront le dessin de votre expression, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 12, + placeHolder: 'Signification inventée 1', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 13, + placeHolder: 'Signification inventée 2', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + + [ + { + title: 'Dessinez votre expression', + description: 'Réalisez votre dessin sur une feuille au format paysage', + inputs: [ + { + id: 14, + type: InputTypeEnum.IMAGE, + selectedValue: '', + isDisplayedInRecap: true, + required: true, + }, + { + id: 15, + label: 'Écrivez l’expression dans la langue que vous avez choisie juste avant', + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + required: true, + }, + { + id: 16, + label: 'Écrivez la traduction “mot à mot” en français', + hidden: { id: 0, value: 'Français' }, + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + isIndice: true, + }, + { + id: 17, + label: 'Que signifie cette expression ?', + placeHolder: 'Signification réelle', + type: InputTypeEnum.INPUT, + selectedValue: '', + isDisplayedInRecap: true, + response: true, + required: true, + }, + ], + }, + { + title: 'Inventez deux significations fausses à cette expression', + description: + 'Vos Pélicopains verront le dessin de votre expression, et devront trouver sa signification parmi la vraie, et ces deux fausses, qu’il faut inventer :', + inputs: [ + { + id: 18, + placeHolder: 'Signification inventée 1', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + { + id: 19, + placeHolder: 'Signification inventée 2', + type: InputTypeEnum.INPUT, + selectedValue: '', + response: false, + isDisplayedInRecap: true, + required: true, + }, + ], + }, + ], + + [ + { + title: 'Pré-visualisez votre activité et publiez-la.', + description: + 'Voici la pré-visualisation de votre activité. Vous pouvez la modifier, et quand vous êtes prêts : publiez-la dans votre village-monde !', + }, + ], + ], + }, +}; diff --git a/src/contexts/gameContext.tsx b/src/contexts/gameContext.tsx new file mode 100644 index 000000000..e357fa47e --- /dev/null +++ b/src/contexts/gameContext.tsx @@ -0,0 +1,80 @@ +import type { ReactNode } from 'react'; +import React, { createContext, useContext, useState } from 'react'; + +import { GAME_FIELDS_CONFIG } from 'src/config/games/game'; +import type { inputType, StepsTypes } from 'types/game.type'; +import { GameType } from 'types/game.type'; + +type GameContextType = { + gameConfig: Array; + setGameConfig: (gameConfig: Array) => void; + gameType?: GameType; + setGameType: (gameType: GameType) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateGameConfig: (value: any, input: inputType) => void; + inputSelectedValue?: string; +}; + +export const GameContext = createContext({ + gameConfig: [], + setGameConfig: () => {}, + gameType: GameType.MIMIC, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setGameType: (_gameType: GameType) => {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars + updateGameConfig: (_value: any, _input: inputType) => {}, + inputSelectedValue: '', +}); + +export const useGame = () => { + const context = useContext(GameContext); + if (!context) { + throw new Error('useGame must be used within a GameProvider'); + } + return context; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const saveGameResponseInSessionStorage = (gameConfig: any) => { + localStorage.setItem('gameConfig', JSON.stringify(gameConfig)); +}; + +interface GameProviderProps { + children: ReactNode; +} + +export const GameProvider = ({ children }: GameProviderProps) => { + const [gameType, setGameType] = useState(GameType.MIMIC); + const [gameConfig, setGameConfig] = useState>(GAME_FIELDS_CONFIG[gameType].steps); + const inputSelectedValue = gameConfig[0]?.[0]?.inputs?.[0]?.selectedValue; + + const updateGameType = (type: GameType) => { + setGameType(type); + setGameConfig(GAME_FIELDS_CONFIG[type].steps); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateGameConfig = (value: any, input: inputType) => { + let inputToCompare: inputType = {} as inputType; + const configCopy = [...gameConfig]; + + configCopy.map((page) => + page.map((step) => + step.inputs?.map((inp) => { + if (inp.id === input.id) { + inputToCompare = inp; + inputToCompare.selectedValue = value; + } + }), + ), + ); + setGameConfig(configCopy); + saveGameResponseInSessionStorage(configCopy); + }; + + return ( + + {children} + + ); +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 02abd811f..87063148b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,6 +37,7 @@ import { NewAdminHeader } from 'src/components/admin/NewAdminHeader'; import { NewAdminNavigation } from 'src/components/admin/NewAdminNavigation'; import { ActivityContextProvider } from 'src/contexts/activityContext'; import { ClassroomContextProvider } from 'src/contexts/classroomContext'; +import { GameProvider } from 'src/contexts/gameContext'; import { MediathequeProvider } from 'src/contexts/mediathequeContext'; import { UserContextProvider } from 'src/contexts/userContext'; import { VillageContextProvider } from 'src/contexts/villageContext'; @@ -138,49 +139,51 @@ const MyApp: React.FunctionComponent & { - {isOnAdmin ? ( - router.pathname.startsWith('/admin/newportal') ? ( - -
- -
- - - - + + {isOnAdmin ? ( + router.pathname.startsWith('/admin/newportal') ? ( + +
+ +
+ + + + +
-
- - ) : ( -
- -
- -
- + + ) : ( +
+ +
+ +
+ +
+ ) + ) : user !== null && + router.pathname !== '/inscription' && + router.pathname !== '/connexion' && + router.pathname !== '/login' && + router.pathname !== '/user-verified' && + router.pathname !== '/reset-password' && + router.pathname !== '/update-password' && + router.pathname !== '/404' ? ( +
+
+ +
- ) - ) : user !== null && - router.pathname !== '/inscription' && - router.pathname !== '/connexion' && - router.pathname !== '/login' && - router.pathname !== '/user-verified' && - router.pathname !== '/reset-password' && - router.pathname !== '/update-password' && - router.pathname !== '/404' ? ( -
-
+ ) : ( - -
- ) : ( - - )} + )} + diff --git a/src/pages/creer-un-jeu/expression/1.tsx b/src/pages/creer-un-jeu/expression/1.tsx new file mode 100644 index 000000000..ed1863fd7 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/1.tsx @@ -0,0 +1,40 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import { BackButton } from 'src/components/buttons/BackButton'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const ExpressionStep1 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/expression/2'); + }; + return ( + +
+ + + +
{}
+
+ + ); +}; + +export default ExpressionStep1; diff --git a/src/pages/creer-un-jeu/expression/2.tsx b/src/pages/creer-un-jeu/expression/2.tsx new file mode 100644 index 000000000..ba384be6e --- /dev/null +++ b/src/pages/creer-un-jeu/expression/2.tsx @@ -0,0 +1,45 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const ExpressionStep2 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/expression/3'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/expression/1`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default ExpressionStep2; diff --git a/src/pages/creer-un-jeu/expression/3.tsx b/src/pages/creer-un-jeu/expression/3.tsx new file mode 100644 index 000000000..dff660054 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/3.tsx @@ -0,0 +1,45 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const ExpressionStep3 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/expression/4'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/expression/2`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default ExpressionStep3; diff --git a/src/pages/creer-un-jeu/expression/4.tsx b/src/pages/creer-un-jeu/expression/4.tsx new file mode 100644 index 000000000..bae7114b3 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/4.tsx @@ -0,0 +1,45 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const ExpressionStep4 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/expression/5'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/expression/3`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default ExpressionStep4; diff --git a/src/pages/creer-un-jeu/expression/5.tsx b/src/pages/creer-un-jeu/expression/5.tsx new file mode 100644 index 000000000..1ba3022af --- /dev/null +++ b/src/pages/creer-un-jeu/expression/5.tsx @@ -0,0 +1,133 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Button, Tooltip, Backdrop, CircularProgress } from '@mui/material'; + +import { postGameDataMonneyOrExpression } from 'src/api/game/game.post'; +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import CreateGame from 'src/components/game/CreateGame'; +import Previsualisation from 'src/components/game/Previsualisation'; +import { GameContext } from 'src/contexts/gameContext'; +import { UserContext } from 'src/contexts/userContext'; +import { VillageContext } from 'src/contexts/villageContext'; +import { getUserDisplayName } from 'src/utils'; +import { ActivityType } from 'types/activity.type'; +import type { StepsTypes, GameDataMonneyOrExpression } from 'types/game.type'; +import { GameType } from 'types/game.type'; +import { UserType } from 'types/user.type'; + +const ExpressionStep5 = () => { + const router = useRouter(); + const { user } = React.useContext(UserContext); + const { village } = React.useContext(VillageContext); + const isObservator = user?.type === UserType.OBSERVATOR; + const { selectedPhase } = React.useContext(VillageContext); + const labelPresentation = user ? getUserDisplayName(user, false) : ''; + const [isLoading, setIsLoading] = React.useState(false); + + const { inputSelectedValue } = useContext(GameContext); + const { gameConfig } = useContext(GameContext); + + const onPublish = async () => { + const data: GameDataMonneyOrExpression = { + userId: user?.id || 0, + villageId: village?.id || 0, + type: ActivityType.GAME, + subType: GameType.EXPRESSION, + game1: { + game: gameConfig[1], + language: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + game2: { + game: gameConfig[2], + language: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + game3: { + game: gameConfig[3], + language: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + selectedPhase: selectedPhase, + }; + + setIsLoading(true); + await postGameDataMonneyOrExpression(data); + localStorage.removeItem('gameConfig'); + router.push('/creer-un-jeu/expression/success'); + setIsLoading(false); + }; + + function validateGameConfig(gameConfig: StepsTypes[][]) { + let isValidGame = true; + + function isEmptyOrSpaces(str: string | undefined) { + return str === null || str === undefined || str.match(/^ *$/) !== null; + } + + gameConfig.forEach((group) => { + group.forEach((item) => { + if (item.inputs) { + item.inputs.forEach((input) => { + if (input.required && (isEmptyOrSpaces(input.selectedValue) || !input.selectedValue)) { + isValidGame = false; + } + }); + } + }); + }); + + return isValidGame; + } + + const isValidGame = validateGameConfig(gameConfig); + return ( + +
+ +
+ +
+ {isObservator ? ( + + + + + + ) : ( + <> + + {!isValidGame ?

Vérifiez tous vos champs s'il vous plaît.

: null} + + )} +
+
+ +
+ + + + + ); +}; + +export default ExpressionStep5; diff --git a/src/pages/creer-un-jeu/expression/displayList.tsx b/src/pages/creer-un-jeu/expression/displayList.tsx new file mode 100644 index 000000000..fe8e3a137 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/displayList.tsx @@ -0,0 +1,20 @@ +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import List from 'src/components/game/List'; +import { VillageContext } from 'src/contexts/villageContext'; + +const DisplayList = () => { + // Rendu du composant + const village = useContext(VillageContext); + return ( + +

Voici la liste des jeux des expressions !

+
+ +
+ + ); +}; + +export default DisplayList; diff --git a/src/pages/creer-un-jeu/expression/index.tsx b/src/pages/creer-un-jeu/expression/index.tsx new file mode 100644 index 000000000..256e99a55 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/index.tsx @@ -0,0 +1,58 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import Button from '@mui/material/Button'; +import { Box } from '@mui/system'; + +import { Base } from 'src/components/Base'; +import { PageLayout } from 'src/components/PageLayout'; + +const Expression = () => { + const router = useRouter(); + + return ( + + +

Qu’est-ce qu’une expression ?

+

+ Une expression est une formule toute faite qu’on utilise le plus souvent à l’oral pour commenter une situation, ou exprimer un jugement. + Pelico aime beaucoup les expressions, car elles sont souvent très imagées... En voyageant, Pelico a remarqué que ces images sont parfois les + mêmes d’une langue à l’autre, et parfois très différentes. Par exemple, pour dire qu’il pleut beaucoup, on peut dire en français “il pleut + comme vache qui pisse” ... alors qu’en anglais on dit “it’s raining cats and dogs” (il pleut des chats et des chiens) +

+

Faites découvrir vos expressions aux Pélicopains !

+
+ À vous de faire découvrir 3 expressions imagées que vous utilisez à vos Pélicopains ! +

+ Vous pourrez d’abord choisir la langue dans laquelle l’expression existe. À chaque étape, vous pourrez ensuite mettre en ligne un dessin + illustrant l’expression, et sa signification. Pour pimenter le jeu, à chaque étape vous devrez également inventer deux significations + fausses … à vos Pélicopains de deviner ce que signifie réellement vos expressions ! +

+
+ + + + + +
+ + ); +}; + +export default Expression; diff --git a/src/pages/creer-un-jeu/expression/jouer/[id].tsx b/src/pages/creer-un-jeu/expression/jouer/[id].tsx new file mode 100644 index 000000000..e57782b61 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/jouer/[id].tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import DisplayGameById from 'src/components/game/DisplayGameById'; + +const DisplayPlayId = () => { + return ( + <> + ; + + ); +}; + +export default DisplayPlayId; diff --git a/src/pages/creer-un-jeu/expression/play.tsx b/src/pages/creer-un-jeu/expression/play.tsx new file mode 100644 index 000000000..b109838c9 --- /dev/null +++ b/src/pages/creer-un-jeu/expression/play.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import Jouer from 'src/components/game/DisplayGameById'; + +const Play = () => { + return ( +
+ +
+ ); +}; + +export default Play; diff --git a/src/pages/creer-un-jeu/expression/success.tsx b/src/pages/creer-un-jeu/expression/success.tsx new file mode 100644 index 000000000..d05de7efe --- /dev/null +++ b/src/pages/creer-un-jeu/expression/success.tsx @@ -0,0 +1,27 @@ +/* eslint-disable @next/next/no-html-link-for-pages */ +import React from 'react'; + +import { Base } from 'src/components/Base'; +import { bgPage } from 'src/styles/variables.const'; +import PelicoSouriant from 'src/svg/pelico/pelico-souriant.svg'; + +const PresentationSuccess = () => { + return ( + +
+

+ {'Vos expressions ont bien été publiées !'} +

+ +

+ {"Revenir à l'accueil"} +

+
+

+ {'Ou découvrez les jeux des autres Pélicopains !'} +

+ + ); +}; + +export default PresentationSuccess; diff --git a/src/pages/creer-un-jeu/index.tsx b/src/pages/creer-un-jeu/index.tsx index 37d141028..e4805fc75 100644 --- a/src/pages/creer-un-jeu/index.tsx +++ b/src/pages/creer-un-jeu/index.tsx @@ -3,23 +3,35 @@ import React from 'react'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; import { ActivityChoice } from 'src/components/activities/ActivityChoice'; +import ExpressionIcon from 'src/svg/jeu/expression.svg'; import MimiqueIcon from 'src/svg/jeu/mimique.svg'; import MonnaieIcon from 'src/svg/jeu/monnaie.svg'; +import { GameType } from 'types/game.type'; const activities = [ { label: 'Jeu des mimiques', href: '/creer-un-jeu/mimique', icon: MimiqueIcon, + gameType: GameType.MIMIC, disabled: false, disabledText: '', }, { label: 'Jeu de la monnaie', - href: '/creer-un-jeu/monnaie', + href: '/creer-un-jeu/objet', icon: MonnaieIcon, - disabled: true, - disabledText: 'Bientôt disponible', + gameType: GameType.MONEY, + disabled: false, + disabledText: '', + }, + { + label: 'Jeu des expressions', + href: '/creer-un-jeu/expression', + icon: ExpressionIcon, + gameType: GameType.EXPRESSION, + disabled: false, + disabledText: '', }, ]; diff --git a/src/pages/creer-un-jeu/mimique/1.tsx b/src/pages/creer-un-jeu/mimique/1.tsx index 73c0125e0..ddf821977 100644 --- a/src/pages/creer-un-jeu/mimique/1.tsx +++ b/src/pages/creer-un-jeu/mimique/1.tsx @@ -1,99 +1,32 @@ import { useRouter } from 'next/router'; import React from 'react'; -import { isGame } from 'src/activity-types/anyActivity'; -import { DEFAULT_MIMIC_DATA, isMimic } from 'src/activity-types/game.constants'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; import { BackButton } from 'src/components/buttons/BackButton'; -import MimicSelector from 'src/components/selectors/MimicSelector'; -import { ActivityContext } from 'src/contexts/activityContext'; -import { UserContext } from 'src/contexts/userContext'; -import { VillageContext } from 'src/contexts/villageContext'; -import { getUserDisplayName } from 'src/utils'; -import { ActivityStatus, ActivityType } from 'types/activity.type'; -import type { MimicData, MimicsData } from 'types/game.type'; -import { GameType } from 'types/game.type'; +import CreateGame from 'src/components/game/CreateGame'; const MimiqueStep1 = () => { const router = useRouter(); - const { activity, updateActivity, createNewActivity } = React.useContext(ActivityContext); - const { user } = React.useContext(UserContext); - const { selectedPhase } = React.useContext(VillageContext); - const labelPresentation = user ? getUserDisplayName(user, false) : ''; - - const created = React.useRef(false); - React.useEffect(() => { - if (!created.current) { - created.current = true; - if (!activity || !('edit' in router.query)) { - createNewActivity(ActivityType.GAME, selectedPhase, GameType.MIMIC, { - ...DEFAULT_MIMIC_DATA, - presentation: labelPresentation, - game1: { - video: '', - signification: '', - origine: '', - fakeSignification1: '', - fakeSignification2: '', - }, - }); - } else if (activity && (!isGame(activity) || !isMimic(activity))) { - createNewActivity(ActivityType.GAME, selectedPhase, GameType.MIMIC, { - ...DEFAULT_MIMIC_DATA, - presentation: labelPresentation, - }); - } - } - }, [activity, labelPresentation, createNewActivity, router, selectedPhase]); - - const isEdit = activity !== null && activity.status !== ActivityStatus.DRAFT; - - const data = (activity?.data as MimicsData) || null; - - const dataChange = (key: keyof MimicData) => (event: React.ChangeEvent) => { - const newData: MimicsData = { ...data }; - (newData.game1[key] as string) = event.target.value; - updateActivity({ data: newData }); - }; - - const videoChange = (newValue: string) => { - const newData = { ...data }; - newData.game1.video = newValue; - updateActivity({ data: newData }); - }; const onNext = () => { router.push('/creer-un-jeu/mimique/2'); }; - - if (!user || !activity || !activity.data || !data.game1) { - return ( - -
- - ); - } - return ( - {!isEdit && } - - - +
+ + + +
{}
+
); diff --git a/src/pages/creer-un-jeu/mimique/2.tsx b/src/pages/creer-un-jeu/mimique/2.tsx index 62221c2a4..532cc5950 100644 --- a/src/pages/creer-un-jeu/mimique/2.tsx +++ b/src/pages/creer-un-jeu/mimique/2.tsx @@ -1,82 +1,35 @@ import { useRouter } from 'next/router'; import React from 'react'; -import { isGame } from 'src/activity-types/anyActivity'; -import { isMimic, isMimicValid } from 'src/activity-types/game.constants'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; import { Steps } from 'src/components/Steps'; -import MimicSelector from 'src/components/selectors/MimicSelector'; -import { ActivityContext } from 'src/contexts/activityContext'; -import type { MimicData, MimicsData } from 'types/game.type'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; const MimiqueStep2 = () => { const router = useRouter(); - const { activity, updateActivity } = React.useContext(ActivityContext); - - const data = (activity?.data as MimicsData) || null; - - const isStep1Valid = React.useMemo(() => { - if (data !== null) { - return isMimicValid(data.game1); - } - return false; - }, [data]); - - React.useEffect(() => { - if (activity === null && !('activity-id' in router.query) && !sessionStorage.getItem('activity')) { - router.push('/creer-un-jeu'); - } else if (activity && (!isGame(activity) || !isMimic(activity))) { - router.push('/creer-un-jeu'); - } - }, [activity, router]); - - const dataChange = (key: keyof MimicData) => (event: React.ChangeEvent) => { - const newData = { ...data }; - (newData.game2[key] as string) = event.target.value; - updateActivity({ data: newData }); - }; - - const videoChange = (newValue: string) => { - const newData = { ...data }; - newData.game2.video = newValue; - updateActivity({ data: newData }); - }; const onNext = () => { router.push('/creer-un-jeu/mimique/3'); }; const onPrev = () => { - router.push(`/creer-un-jeu/mimique/1?edit=${activity?.id}`); + router.push(`/creer-un-jeu/mimique/1`); }; - if (!activity || data === null) { - return ( - -
- - ); - } - return ( - - +
+ +
+
{}
); diff --git a/src/pages/creer-un-jeu/mimique/3.tsx b/src/pages/creer-un-jeu/mimique/3.tsx index 185d4620e..236b7c7f3 100644 --- a/src/pages/creer-un-jeu/mimique/3.tsx +++ b/src/pages/creer-un-jeu/mimique/3.tsx @@ -1,83 +1,35 @@ import { useRouter } from 'next/router'; import React from 'react'; -import { isGame } from 'src/activity-types/anyActivity'; -import { isMimic, isMimicValid } from 'src/activity-types/game.constants'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; import { Steps } from 'src/components/Steps'; -import MimicSelector from 'src/components/selectors/MimicSelector'; -import { ActivityContext } from 'src/contexts/activityContext'; -import type { MimicData, MimicsData } from 'types/game.type'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; const MimiqueStep3 = () => { const router = useRouter(); - const { activity, updateActivity } = React.useContext(ActivityContext); - - const data = (activity?.data as MimicsData) || null; - - const errorSteps = React.useMemo(() => { - const errors: number[] = []; - if (data === undefined || data === null) return; //when you came from ma-classe.tsx - if (!isMimicValid(data.game1)) errors.push(0); // step of mimic 1 - if (!isMimicValid(data.game2)) errors.push(1); // step of mimic 2 - return errors; - }, [data]); - - React.useEffect(() => { - if (activity === null && !('activity-id' in router.query) && !sessionStorage.getItem('activity')) { - router.push('/creer-un-jeu'); - } else if (activity && (!isGame(activity) || !isMimic(activity))) { - router.push('/creer-un-jeu'); - } - }, [activity, router]); - - const dataChange = (key: keyof MimicData) => (event: React.ChangeEvent) => { - const newData = { ...data }; - (newData.game3[key] as string) = event.target.value; - updateActivity({ data: newData }); - }; - - const videoChange = (newValue: string) => { - const newData = { ...data }; - newData.game3.video = newValue; - updateActivity({ data: newData }); - }; const onNext = () => { router.push('/creer-un-jeu/mimique/4'); }; const onPrev = () => { - router.push('/creer-un-jeu/mimique/2'); + router.push(`/creer-un-jeu/mimique/2`); }; - if (!activity || data === null) { - return ( - -
- - ); - } - return ( - - +
+ +
+
{}
); diff --git a/src/pages/creer-un-jeu/mimique/4.tsx b/src/pages/creer-un-jeu/mimique/4.tsx index d9c6bfa3c..fd9cd56a0 100644 --- a/src/pages/creer-un-jeu/mimique/4.tsx +++ b/src/pages/creer-un-jeu/mimique/4.tsx @@ -1,91 +1,95 @@ -import classNames from 'classnames'; import { useRouter } from 'next/router'; -import React from 'react'; -import ReactPlayer from 'react-player'; -import type { SourceProps } from 'react-player/base'; +import React, { useContext } from 'react'; -import { Grid, Button, Radio, RadioGroup, FormControlLabel, Backdrop, CircularProgress, Tooltip } from '@mui/material'; +import { Button, Tooltip, Backdrop, CircularProgress } from '@mui/material'; -import { isGame } from 'src/activity-types/anyActivity'; -import { isMimic, isMimicValid } from 'src/activity-types/game.constants'; +import { postGameDataMonneyOrExpression } from 'src/api/game/game.post'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; import { Steps } from 'src/components/Steps'; -import { CustomRadio } from 'src/components/buttons/CustomRadio'; -import { EditButton } from 'src/components/buttons/EditButton'; -import { ActivityContext } from 'src/contexts/activityContext'; +import CreateGame from 'src/components/game/CreateGame'; +import Previsualisation from 'src/components/game/Previsualisation'; +import { GameContext } from 'src/contexts/gameContext'; import { UserContext } from 'src/contexts/userContext'; -import type { MimicsData } from 'types/game.type'; +import { VillageContext } from 'src/contexts/villageContext'; +import { getUserDisplayName } from 'src/utils'; +import { ActivityType } from 'types/activity.type'; +import type { StepsTypes, GameDataMonneyOrExpression } from 'types/game.type'; +import { GameType } from 'types/game.type'; import { UserType } from 'types/user.type'; const MimiqueStep4 = () => { const router = useRouter(); - const { activity, save } = React.useContext(ActivityContext); const { user } = React.useContext(UserContext); - const [isLoading, setIsLoading] = React.useState(false); - const data = (activity?.data as MimicsData) || null; + const { village } = React.useContext(VillageContext); const isObservator = user?.type === UserType.OBSERVATOR; + const { selectedPhase } = React.useContext(VillageContext); + const labelPresentation = user ? getUserDisplayName(user, false) : ''; + const [isLoading, setIsLoading] = React.useState(false); - const errorSteps = React.useMemo(() => { - const errors: number[] = []; - if (data === undefined || data === null) return; //when you came from ma-classe.tsx - if (!isMimicValid(data.game1)) errors.push(0); // step of mimic 1 - if (!isMimicValid(data.game2)) errors.push(1); // step of mimic 2 - if (!isMimicValid(data.game3)) errors.push(2); // step of mimic 3 - return errors; - }, [data]); - - const isValid = errorSteps?.length === 0; - - React.useEffect(() => { - if (activity === null && !('activity-id' in router.query) && !sessionStorage.getItem('activity')) { - router.push('/creer-un-jeu'); - } else if (activity && (!isGame(activity) || !isMimic(activity))) { - router.push('/creer-un-jeu'); - } - }, [activity, router]); + const { gameConfig } = useContext(GameContext); const onPublish = async () => { - //Reset error in step to not show error in next mimic creation - window.sessionStorage.removeItem(`mimic-step-1ère-next`); - window.sessionStorage.removeItem(`mimic-step-2ème-next`); - window.sessionStorage.removeItem(`mimic-step-3ème-next`); + const data: GameDataMonneyOrExpression = { + userId: user?.id || 0, + villageId: village?.id || 0, + type: ActivityType.GAME, + subType: GameType.MIMIC, + game1: { + game: gameConfig[0], + labelPresentation: labelPresentation, + }, + game2: { + game: gameConfig[1], + labelPresentation: labelPresentation, + }, + game3: { + game: gameConfig[2], + labelPresentation: labelPresentation, + }, + selectedPhase: selectedPhase, + }; + setIsLoading(true); - const { success } = await save(true); - if (success) { - router.push('/creer-un-jeu/mimique/success'); - } + await postGameDataMonneyOrExpression(data); + localStorage.removeItem('gameConfig'); + router.push('/creer-un-jeu/mimique/success'); setIsLoading(false); }; - if (!activity) { - return ( - -
- - ); + function validateGameConfig(gameConfig: StepsTypes[][]) { + let isValidGame = true; + + function isEmptyOrSpaces(str: string | undefined) { + return str === null || str === undefined || str.match(/^ *$/) !== null; + } + + gameConfig.forEach((group) => { + group.forEach((item) => { + if (item.inputs) { + item.inputs.forEach((input) => { + if (input.required && (isEmptyOrSpaces(input.selectedValue) || !input.selectedValue)) { + isValidGame = false; + } + }); + } + }); + }); + + return isValidGame; } + const isValidGame = validateGameConfig(gameConfig); return ( -
-

Pré-visualisez vos mimiques et publiez les !

-

- Vous pouvez modifier chaque mimique si vous le souhaitez. Quand vous êtes prêts : -

- {!isValid && ( -

- Avant de publier votre présentation, il faut corriger les étapes incomplètes, marquées en orange. -

- )} +
{isObservator ? ( @@ -96,144 +100,16 @@ const MimiqueStep4 = () => { ) : ( - + <> + + {!isValidGame ?

Vérifiez tous vos champs s'il vous plaît.

: null} + )}
- {/* Mimique 1 */} -
- - - - - - - } - label={errorSteps?.includes(0) ? '' : data.game1.signification || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(0) ? '' : data.game1.fakeSignification1 || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(0) ? '' : data.game1.fakeSignification2 || ''} - style={{ maxWidth: '100%' }} - /> - - - - { - router.push(`/creer-un-jeu/mimique/1?edit=${activity.id}`); - }} - status={errorSteps?.includes(0) ? 'warning' : 'success'} - style={{ position: 'absolute', top: '40%', right: '0.5rem' }} - /> - - -

{data.game1.origine}

-
- {/* Mimique 2 */} -
- - - - - - - } - label={errorSteps?.includes(1) ? '' : data.game2.signification || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(1) ? '' : data.game2.fakeSignification1 || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(1) ? '' : data.game2.fakeSignification2 || ''} - style={{ maxWidth: '100%' }} - /> - - - - { - router.push('/creer-un-jeu/mimique/2'); - }} - status={errorSteps?.includes(1) ? 'warning' : 'success'} - style={{ position: 'absolute', top: '40%', right: '0.5rem' }} - /> - - -

{data.game2.origine}

-
- {/* Mimique 3 */} -
- - - - - - - } - label={errorSteps?.includes(2) ? '' : data.game3.signification || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(2) ? '' : data.game3.fakeSignification1 || ''} - style={{ maxWidth: '100%' }} - /> - } - label={errorSteps?.includes(2) ? '' : data.game3.fakeSignification2 || ''} - style={{ maxWidth: '100%' }} - /> - - - - { - router.push('/creer-un-jeu/mimique/3'); - }} - status={errorSteps?.includes(2) ? 'warning' : 'success'} - style={{ position: 'absolute', top: '40%', right: '0.5rem' }} - /> - - -

{data.game3.origine}

-
+
diff --git a/src/pages/creer-un-jeu/mimique/displayList.tsx b/src/pages/creer-un-jeu/mimique/displayList.tsx new file mode 100644 index 000000000..373969f49 --- /dev/null +++ b/src/pages/creer-un-jeu/mimique/displayList.tsx @@ -0,0 +1,19 @@ +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import List from 'src/components/game/List'; +import { VillageContext } from 'src/contexts/villageContext'; + +const DisplayList = () => { + const village = useContext(VillageContext); + return ( + +

Voici la liste des jeux des mimiques !

+
+ +
+ + ); +}; + +export default DisplayList; diff --git a/src/pages/creer-un-jeu/mimique/index.tsx b/src/pages/creer-un-jeu/mimique/index.tsx index 405a84d57..079c227d8 100644 --- a/src/pages/creer-un-jeu/mimique/index.tsx +++ b/src/pages/creer-un-jeu/mimique/index.tsx @@ -1,15 +1,22 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; -import React from 'react'; +import React, { useContext, useEffect } from 'react'; import Button from '@mui/material/Button'; import { Box } from '@mui/system'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; +import { GameContext } from 'src/contexts/gameContext'; +import { GameType } from 'types/game.type'; const Mimique = () => { const router = useRouter(); + const { setGameType } = useContext(GameContext); + + useEffect(() => { + setGameType(GameType.MIMIC); + }, [setGameType]); return ( diff --git a/src/pages/creer-un-jeu/mimique/jouer.tsx b/src/pages/creer-un-jeu/mimique/jouer.tsx deleted file mode 100644 index 33bd90d26..000000000 --- a/src/pages/creer-un-jeu/mimique/jouer.tsx +++ /dev/null @@ -1,451 +0,0 @@ -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import React, { useState, useCallback, useMemo, useContext, useEffect } from 'react'; - -import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import ShuffleIcon from '@mui/icons-material/Shuffle'; -import type { SvgIconTypeMap } from '@mui/material'; -import { Box, Button, FormControlLabel, Grid, Radio, RadioGroup } from '@mui/material'; -import type { OverridableComponent } from '@mui/material/OverridableComponent'; - -import { AvatarImg } from 'src/components/Avatar'; -import { Base } from 'src/components/Base'; -import { Flag } from 'src/components/Flag'; -import { Modal } from 'src/components/Modal'; -import { PageLayout } from 'src/components/PageLayout'; -import { UserDisplayName } from 'src/components/UserDisplayName'; -import { RightNavigation } from 'src/components/accueil/RightNavigation'; -import GameStats from 'src/components/activities/GameStats'; -import { VideoView } from 'src/components/activities/content/views/VideoView'; -import ResponseButton from 'src/components/buttons/GameResponseButton'; -import { UserContext } from 'src/contexts/userContext'; -import { VillageContext } from 'src/contexts/villageContext'; -import { useGameRequests } from 'src/services/useGames'; -import { useVillageUsers } from 'src/services/useVillageUsers'; -import { primaryColor } from 'src/styles/variables.const'; -import PelicoNeutre from 'src/svg/pelico/pelico_neutre.svg'; -import { GameType } from 'types/game.type'; -import type { Game, MimicData } from 'types/game.type'; -import type { GameResponse } from 'types/gameResponse.type'; -import { GameResponseValue } from 'types/gameResponse.type'; -import { UserType } from 'types/user.type'; - -function shuffleArray(array: Array) { - let i = array.length - 1; - for (; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - return array; -} - -/* partie à déplacer et à centraliser avec les prochains jeux*/ -type AlreadyPlayerModalProps = { - isOpen: boolean; - gameId: number; - handleSuccessClick: () => void; -}; - -enum RadioBoxValues { - NEW = 'Nouvelle', - RANDOM = 'Aléatoire', -} - -type RadioNextGameProps = { - value: RadioBoxValues; - Icon: OverridableComponent> & { - muiName: string; - }; - onChange: (event: React.SyntheticEvent) => void; - checked: boolean; -}; - -const radioListComponentMapper = { - [RadioBoxValues.NEW]: AccessTimeIcon, - [RadioBoxValues.RANDOM]: ShuffleIcon, -}; - -const RadioNextGame: React.FC = ({ value, Icon, onChange, checked }) => ( - } - label={ - <> - {value} - - } - labelPlacement="end" - checked={checked} - onChange={onChange} - /> -); - -const AlreadyPlayerModal: React.FC = ({ isOpen, handleSuccessClick }) => { - const router = useRouter(); - - return ( - router.push('/')} - onConfirm={handleSuccessClick} - > - C’était la dernière mimique disponible ! Dès que de nouvelles mimiques sont ajoutées, cela apparaîtra dans le fil d’activité. - - ); -}; -/* FIN partie à déplacer et à centraliser avec les prochains jeux*/ - -const PlayMimic = () => { - const { user } = useContext(UserContext); - const { village } = useContext(VillageContext); - const { users } = useVillageUsers(); - const { getRandomGame, sendNewGameResponse, getGameStats, getAvailableGames, resetGamesPlayedForUser } = useGameRequests(); - - const [game, setGame] = useState(undefined); - const [tryCount, setTryCount] = useState(0); - const [found, setFound] = useState(false); - const [errorModalOpen, setErrorModalOpen] = useState(false); - const [isLastMimicModalOpen, setIsLastMimicModalOpen] = useState(false); - const [loadingGame, setLoadingGame] = useState(true); - const [gameResponses, setGameResponses] = useState([]); - const [selectedValue, setSelectedValue] = useState(RadioBoxValues.NEW); - const router = useRouter(); - - const handleConfirmModal = async () => { - const success = await resetGamesPlayedForUser(); - setIsLastMimicModalOpen(false); - - if (success) { - router.reload(); - } else { - router.push('/'); - } - }; - - const sortGamesByDescOrder = (games: Game[]) => { - return games.sort((a, b) => { - const dateA = a.createDate ? new Date(a.createDate).getTime() : 0; - const dateB = b.createDate ? new Date(b.createDate).getTime() : 0; - return dateB - dateA; - }); - }; - - const getNextGame = useCallback(async () => { - setLoadingGame(true); - - // [1] Reset game. - setFound(false); - setGameResponses([]); - setTryCount(0); - setErrorModalOpen(false); - - const availableGames = await getAvailableGames(GameType.MIMIC); - const availableGamesByDescOrder = sortGamesByDescOrder(availableGames); - - const currentGameIndex = availableGamesByDescOrder.findIndex((g) => g.id === game?.id); - - const isLastGame = currentGameIndex === availableGamesByDescOrder.length - 1; - - const NEXT_GAME_MAPPER = { - [RadioBoxValues.NEW]: () => availableGamesByDescOrder[currentGameIndex + 1], - [RadioBoxValues.RANDOM]: async () => { - return await getRandomGame(GameType.MIMIC); - }, - }; - - const nextGame = isLastGame ? undefined : await NEXT_GAME_MAPPER[selectedValue](); - - setGame(nextGame); - if (isLastGame) { - setIsLastMimicModalOpen(isLastGame); - } - - setLoadingGame(false); - }, [getRandomGame, selectedValue, game, getAvailableGames]); - - // Get next game on start and on village change. - useEffect(() => { - const getFirstGame = async () => { - try { - const availableGames = await getAvailableGames(GameType.MIMIC); - if (availableGames.length === 0) { - setIsLastMimicModalOpen(true); - } - const availableGamesByDescOrder = sortGamesByDescOrder(availableGames); - setGame(availableGamesByDescOrder[0]); // Utiliser le dernier jeu (plus récent) - setLoadingGame(false); - } catch (error) { - console.error('Error fetching or processing available games:', error); - setLoadingGame(false); // Assurez-vous de gérer les erreurs correctement - } - }; - getFirstGame().catch(); - }, [getAvailableGames]); - - const userMap = useMemo( - () => - users.reduce<{ [key: number]: number }>((acc, u, index) => { - acc[u.id] = index; - return acc; - }, {}), - [users], - ); - const mimicContent = useMemo(() => { - if (game === undefined) { - return undefined; - } - try { - const { id, origine, fakeSignification1, fakeSignification2, signification, video } = game; - const content: MimicData = { gameId: id, createDate: new Date(), origine, fakeSignification1, fakeSignification2, signification, video }; - return content; - } catch (e) { - return undefined; - } - }, [game]); - - const ResponseButtonDataMapper = useMemo( - () => [ - { - value: GameResponseValue.SIGNIFICATION, - signification: mimicContent?.signification, - isSuccess: true, - isGreenRadio: true, - }, - { - value: GameResponseValue.FAKE_SIGNIFICATION_1, - signification: mimicContent?.fakeSignification1, - }, - { - value: GameResponseValue.FAKE_SIGNIFICATION_2, - signification: mimicContent?.fakeSignification2, - }, - ], - [mimicContent], - ); - - const gameCreator = useMemo(() => { - if (game === undefined) { - return undefined; - } - return userMap[game.userId] !== undefined ? users[userMap[game.userId]] : undefined; - }, [game, userMap, users]); - const gameCreatorIsPelico = gameCreator !== undefined && gameCreator.type <= UserType.MEDIATOR; - const userIsPelico = user !== null && user.type <= UserType.MEDIATOR; - - const choices = React.useMemo(() => (game !== undefined ? shuffleArray([0, 1, 2]) : [0, 1, 2]), [game]); - - const handleRadioButtonChange = (event: React.SyntheticEvent) => { - const selected = (event as React.ChangeEvent).target.value; - setSelectedValue(selected as RadioBoxValues); - if (selected === RadioBoxValues.RANDOM) { - setLoadingGame(true); - getNextGame(); - } - }; - - const handleClick = useCallback( - async (selection: GameResponseValue, isSuccess: boolean = false) => { - if (game === undefined) { - return; - } - const apiResponse = await sendNewGameResponse(game.id, selection); - if (!apiResponse) { - console.error('Error reaching server'); - return; - } - - setFound(isSuccess); - setErrorModalOpen(!isSuccess); - if (isSuccess || tryCount === 1) { - setGameResponses(await getGameStats(game.id)); - } - setTryCount(tryCount + 1); - }, - [getGameStats, sendNewGameResponse, setFound, setErrorModalOpen, setGameResponses, setTryCount, tryCount, game], - ); - - if (user === null || village === null || loadingGame) { - return ; - } - - // Modal dernière mimique - if (!game || !gameCreator) { - return ( - - - - ); - } - - return ( - } hideLeftNav showSubHeader> - - - -

Jouer au jeu des mimiques !

-

A vous de décider avec quelle mimique vous allez jouer !

-

- Vous pouvez choisir comment les faire défiler ou les afficher, par défaut vous visualisez la dernière mimique publiée. -

-
-
- -
-

- {'Une mimique proposée par '} - -

- {mimicContent && mimicContent.createDate && ( -

- {'publié le '} - {new Date(mimicContent.createDate).toLocaleDateString()} - {gameCreatorIsPelico ? ( - - ) : ( - gameCreator && gameCreator.country && - )} -

- )} -
-
- - - {Object.keys(radioListComponentMapper).map((value: string, index: number) => { - return ( - - ); - })} - - - - - {mimicContent !== undefined && mimicContent.video !== null && } - - -

Que signifie cette mimique ?

-
-
- {choices && - choices.map((val) => { - const { value, isSuccess, signification } = ResponseButtonDataMapper[val]; - const isCorrect = isSuccess && found; - const mimicOrigine = mimicContent?.origine || ''; - const isDisabled = (isSuccess && tryCount > 1) || (!isSuccess && found); - return ( -
- handleClick(value, isSuccess)} - isSuccess={isSuccess} - signification={signification} - disabled={isDisabled} - isCorrect={isCorrect || (tryCount > 1 && isSuccess)} - mimicOrigine={mimicOrigine} - /> -
- ); - })} - {(found || tryCount > 1) && ( - <> - {user.country && ( - - )} - c.isoCode).find((i) => i !== user.country?.isoCode) || '' - } - userMap={userMap} - users={users} - /> - - )} -
- - - {found &&

C’est exact ! Vous avez trouvé la signification de cette mimique.

} -
-
- {/* */} -
- 1 && !found ? 'Fermer' : 'Réessayer'} - maxWidth="lg" - ariaDescribedBy="new-user-desc" - ariaLabelledBy="new-user-title" - onClose={() => setErrorModalOpen(false)} - > - {tryCount > 1 && !found ? ( -

Dommage ! Vous n’avez pas trouvé la bonne réponse cette fois-ci.

- ) : ( -

Dommage ! Ce n’est pas cette réponse. Essayez encore !

- )} -
- - - - {(found || tryCount > 1) && ( -
-

- - {"Ou revenir à l'accueil"} - -

-
- )} -
- - - -
-
- - ); -}; - -export default PlayMimic; diff --git a/src/pages/creer-un-jeu/mimique/jouer/[id].tsx b/src/pages/creer-un-jeu/mimique/jouer/[id].tsx new file mode 100644 index 000000000..85bf0e547 --- /dev/null +++ b/src/pages/creer-un-jeu/mimique/jouer/[id].tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import DisplayGameById from 'src/components/game/DisplayGameById'; + +const DisplayPlayId = () => { + return ( + <> + ; + + ); +}; + +export default DisplayPlayId; diff --git a/src/pages/creer-un-jeu/mimique/success.tsx b/src/pages/creer-un-jeu/mimique/success.tsx index a538d4121..f15f3b6f0 100644 --- a/src/pages/creer-un-jeu/mimique/success.tsx +++ b/src/pages/creer-un-jeu/mimique/success.tsx @@ -1,65 +1,27 @@ -import Link from 'next/link'; -import { useRouter } from 'next/router'; +/* eslint-disable @next/next/no-html-link-for-pages */ import React from 'react'; -import Button from '@mui/material/Button'; - -import { isGame } from 'src/activity-types/anyActivity'; import { Base } from 'src/components/Base'; import { PageLayout } from 'src/components/PageLayout'; -import { ActivityContext } from 'src/contexts/activityContext'; -import { useGameRequests } from 'src/services/useGames'; import { bgPage } from 'src/styles/variables.const'; import PelicoSouriant from 'src/svg/pelico/pelico-souriant.svg'; -import { GameType } from 'types/game.type'; const PresentationSuccess = () => { - const router = useRouter(); - const { activity } = React.useContext(ActivityContext); - const { getAvailableGamesCount } = useGameRequests(); - const [hasMimicsAvailable, setHasMimicsAvailable] = React.useState(false); - - if (!activity || !isGame(activity)) { - router.push('/creer-un-jeu/mimique'); - } - - React.useEffect(() => { - getAvailableGamesCount(GameType.MIMIC).then((count) => { - setHasMimicsAvailable(count > 0); - }); - }, [getAvailableGamesCount]); - return (
-

{'Vos mimiques ont bien été publiées !'}

+

+ {'Vos mimiques ont bien été publiées !'} +

- {hasMimicsAvailable ? ( -

- - {"Revenir à l'accueil"} - -

- ) : ( -

{''}

- )} -
-
- {hasMimicsAvailable ? ( - - - - ) : ( - - - - )} +

+ {"Revenir à l'accueil"} +

+

+ {'Ou découvrez les jeux des autres Pélicopains !'} +

); diff --git a/src/pages/creer-un-jeu/objet/1.tsx b/src/pages/creer-un-jeu/objet/1.tsx new file mode 100644 index 000000000..45bd153e1 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/1.tsx @@ -0,0 +1,35 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import { BackButton } from 'src/components/buttons/BackButton'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const MonnaieStep1 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/objet/2'); + }; + + return ( + +
+ + + +
{}
+
+ + ); +}; + +export default MonnaieStep1; diff --git a/src/pages/creer-un-jeu/objet/2.tsx b/src/pages/creer-un-jeu/objet/2.tsx new file mode 100644 index 000000000..17fd03e57 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/2.tsx @@ -0,0 +1,39 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const MonnaieStep2 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/objet/3'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/objet/1`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default MonnaieStep2; diff --git a/src/pages/creer-un-jeu/objet/3.tsx b/src/pages/creer-un-jeu/objet/3.tsx new file mode 100644 index 000000000..7584226d7 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/3.tsx @@ -0,0 +1,39 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const MonnaieStep3 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/objet/4'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/objet/2`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default MonnaieStep3; diff --git a/src/pages/creer-un-jeu/objet/4.tsx b/src/pages/creer-un-jeu/objet/4.tsx new file mode 100644 index 000000000..306746849 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/4.tsx @@ -0,0 +1,39 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import { StepsButton } from 'src/components/StepsButtons'; +import CreateGame from 'src/components/game/CreateGame'; +import { GameContext } from 'src/contexts/gameContext'; + +const MonnaieStep4 = () => { + const router = useRouter(); + const { inputSelectedValue } = useContext(GameContext); + + const onNext = () => { + router.push('/creer-un-jeu/objet/5'); + }; + + const onPrev = () => { + router.push(`/creer-un-jeu/objet/3`); + }; + + return ( + +
+ +
+ +
+
{}
+
+ + ); +}; + +export default MonnaieStep4; diff --git a/src/pages/creer-un-jeu/objet/5.tsx b/src/pages/creer-un-jeu/objet/5.tsx new file mode 100644 index 000000000..60c9d7a73 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/5.tsx @@ -0,0 +1,125 @@ +import { useRouter } from 'next/router'; +import React, { useContext } from 'react'; + +import { Button, Tooltip, Backdrop, CircularProgress } from '@mui/material'; + +import { postGameDataMonneyOrExpression } from 'src/api/game/game.post'; +import { Base } from 'src/components/Base'; +import { Steps } from 'src/components/Steps'; +import CreateGame from 'src/components/game/CreateGame'; +import Previsualisation from 'src/components/game/Previsualisation'; +import { GameContext } from 'src/contexts/gameContext'; +import { UserContext } from 'src/contexts/userContext'; +import { VillageContext } from 'src/contexts/villageContext'; +import { getUserDisplayName } from 'src/utils'; +import { ActivityType } from 'types/activity.type'; +import type { StepsTypes, GameDataMonneyOrExpression } from 'types/game.type'; +import { GameType } from 'types/game.type'; +import { UserType } from 'types/user.type'; + +const MonnaieStep5 = () => { + const router = useRouter(); + const { user } = React.useContext(UserContext); + const { village } = React.useContext(VillageContext); + const isObservator = user?.type === UserType.OBSERVATOR; + const { selectedPhase } = React.useContext(VillageContext); + const labelPresentation = user ? getUserDisplayName(user, false) : ''; + const [isLoading, setIsLoading] = React.useState(false); + + const { inputSelectedValue } = useContext(GameContext); + const { gameConfig } = useContext(GameContext); + + const onPublish = async () => { + const data: GameDataMonneyOrExpression = { + userId: user?.id || 0, + villageId: village?.id || 0, + type: ActivityType.GAME, + subType: GameType.MONEY, + game1: { + game: gameConfig[1], + monney: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + game2: { + game: gameConfig[2], + monney: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + game3: { + game: gameConfig[3], + monney: inputSelectedValue, + labelPresentation: labelPresentation, + radio: gameConfig?.[0]?.[1]?.inputs?.[0]?.selectedValue, + }, + selectedPhase: selectedPhase, + }; + + setIsLoading(true); + await postGameDataMonneyOrExpression(data); + localStorage.removeItem('gameConfig'); + router.push('/creer-un-jeu/objet/success'); + setIsLoading(false); + }; + + function validateGameConfig(gameConfig: StepsTypes[][]) { + let isValidGame = true; + + function isEmptyOrSpaces(str: string | undefined) { + return str === null || str === undefined || str.match(/^ *$/) !== null; + } + + gameConfig.forEach((group) => { + group.forEach((item) => { + if (item.inputs) { + item.inputs.forEach((input) => { + if (input.required && (isEmptyOrSpaces(input.selectedValue) || !input.selectedValue)) { + isValidGame = false; + } + }); + } + }); + }); + + return isValidGame; + } + + const isValidGame = validateGameConfig(gameConfig); + return ( + +
+ + +
+ {isObservator ? ( + + + + + + ) : ( + <> + + {!isValidGame ?

Vérifiez tous vos champs s'il vous plaît.

: null} + + )} +
+ +
+ + + + + ); +}; + +export default MonnaieStep5; diff --git a/src/pages/creer-un-jeu/objet/displayList.tsx b/src/pages/creer-un-jeu/objet/displayList.tsx new file mode 100644 index 000000000..26fe9a461 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/displayList.tsx @@ -0,0 +1,20 @@ +import React, { useContext } from 'react'; + +import { Base } from 'src/components/Base'; +import List from 'src/components/game/List'; +import { VillageContext } from 'src/contexts/villageContext'; + +const DisplayList = () => { + // Rendu du composant + const village = useContext(VillageContext); + return ( + +

Voici la liste des jeux des objets !

+
+ +
+ + ); +}; + +export default DisplayList; diff --git a/src/pages/creer-un-jeu/objet/index.tsx b/src/pages/creer-un-jeu/objet/index.tsx new file mode 100644 index 000000000..b9e6ce395 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/index.tsx @@ -0,0 +1,56 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import Button from '@mui/material/Button'; +import { Box } from '@mui/system'; + +import { Base } from 'src/components/Base'; +import { PageLayout } from 'src/components/PageLayout'; + +const Monnaie = () => { + const router = useRouter(); + + return ( + + +

Jeu de la monnaie

+

+ Savez-vous qu’il existe beaucoup de types de monnaies différentes selon les pays où vous vous rendez ? En fonction du type de monnaie les + billets et les pièces ne seront pas les mêmes. Cela veut dire qu’elles n’ont pas les mêmes dessins ou couleurs mais aussi qu’ils ne vont pas + représenter la même somme d’argent. +

+

Faites découvrir votre monnaie aux Pélicopains !

+
+ À vous de faire découvrir la valeur de 3 objets que vous utilisez à vos Pélicopains ! +

+ Dans ses voyages Pélico s’est rendu compte qu’un même objet ne coûte pas le même prix dans différents pays. Il s’amuse donc à essayer de + deviner combien coûte des objets du quotidien dans chaque pays qu’il visite. C’est le jeu qu’il vous propose de faire aujourd’hui ! +

+
+ + + + + +
+ + ); +}; + +export default Monnaie; diff --git a/src/pages/creer-un-jeu/objet/jouer/[id].tsx b/src/pages/creer-un-jeu/objet/jouer/[id].tsx new file mode 100644 index 000000000..216287fa4 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/jouer/[id].tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import DisplayGameById from 'src/components/game/DisplayGameById'; + +const DisplayPlayId = () => { + return ( + <> + ; + + ); +}; + +export default DisplayPlayId; diff --git a/src/pages/creer-un-jeu/objet/play.tsx b/src/pages/creer-un-jeu/objet/play.tsx new file mode 100644 index 000000000..08008ad80 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/play.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import Jouer from 'src/components/game/DisplayGameById'; + +const Play = () => { + return ( +
+ +
+ ); +}; + +export default Play; diff --git a/src/pages/creer-un-jeu/objet/success.tsx b/src/pages/creer-un-jeu/objet/success.tsx new file mode 100644 index 000000000..206a6be38 --- /dev/null +++ b/src/pages/creer-un-jeu/objet/success.tsx @@ -0,0 +1,27 @@ +/* eslint-disable @next/next/no-html-link-for-pages */ +import React from 'react'; + +import { Base } from 'src/components/Base'; +import { bgPage } from 'src/styles/variables.const'; +import PelicoSouriant from 'src/svg/pelico/pelico-souriant.svg'; + +const PresentationSuccess = () => { + return ( + +
+

+ {'Vos objets ont bien été publiés !'} +

+ +

+ {"Revenir à l'accueil"} +

+
+

+ {'Ou découvrez les jeux des autres Pélicopains !'} +

+ + ); +}; + +export default PresentationSuccess; diff --git a/src/pages/familles/1.tsx b/src/pages/familles/1.tsx index 3de5195b5..f66813913 100644 --- a/src/pages/familles/1.tsx +++ b/src/pages/familles/1.tsx @@ -12,7 +12,7 @@ import { Steps } from 'src/components/Steps'; import { StepsButton } from 'src/components/StepsButtons'; import type { FilterArgs } from 'src/components/accueil/Filters'; import { Filters } from 'src/components/accueil/Filters'; -import { filterActivitiesByTerm, filterActivitiesWithLastMimicGame } from 'src/components/accueil/Filters/FilterActivities'; +import { filterActivitiesByTerm, filterActivitiesWithLastGame } from 'src/components/accueil/Filters/FilterActivities'; import { ActivityCard } from 'src/components/activities/ActivityCard'; import { BackButton } from 'src/components/buttons/BackButton'; import { ClassroomContext } from 'src/contexts/classroomContext'; @@ -276,7 +276,7 @@ const ClassroomParamStep1 = () => { const activitiesFiltered = React.useMemo(() => { if (activities && activities.length > 0) { - const activitiesWithLastMimic = filterActivitiesWithLastMimicGame(activities); + const activitiesWithLastMimic = filterActivitiesWithLastGame(activities); const activitiesFilterBySearchTerm = filters.searchTerm.length > 0 ? filterActivitiesByTerm(activitiesWithLastMimic, filters.searchTerm) : activitiesWithLastMimic; return activitiesFilterBySearchTerm; diff --git a/src/pages/lancer-un-defi/linguistique/1.tsx b/src/pages/lancer-un-defi/linguistique/1.tsx index a05720202..dd269e8c5 100644 --- a/src/pages/lancer-un-defi/linguistique/1.tsx +++ b/src/pages/lancer-un-defi/linguistique/1.tsx @@ -181,6 +181,7 @@ const DefiStep1 = () => { + {data.languageCode && (

@@ -195,6 +196,7 @@ const DefiStep1 = () => {

)}
+ {}
diff --git a/src/pages/ma-classe.tsx b/src/pages/ma-classe.tsx index b0bc3b0b4..ee235f900 100644 --- a/src/pages/ma-classe.tsx +++ b/src/pages/ma-classe.tsx @@ -6,6 +6,7 @@ import { Base } from 'src/components/Base'; import { Modal } from 'src/components/Modal'; import { PageLayout } from 'src/components/PageLayout'; import { ActivityCard } from 'src/components/activities/ActivityCard'; +import { GameCardMaClasse } from 'src/components/activities/ActivityCard/GameCardMaClasse'; import { MascotteTemplate } from 'src/components/activities/content/MascotteTemplate'; import { ActivityContext } from 'src/contexts/activityContext'; import { UserContext } from 'src/contexts/userContext'; @@ -89,6 +90,7 @@ const MaClasse = () => { }; const hasNoPublishedActivities = activities.filter((a) => a.userId === user?.id && !isMascotte(a)).length === 0; + const hasGamesInActivities = activities.filter((a) => a.userId === user?.id && a.type === 4).length > 0; return ( @@ -128,12 +130,33 @@ const MaClasse = () => { ) : null, ) )} +

Mes jeux

+ {hasGamesInActivities ? ( + activities.map((activity, index) => + user && activity.userId === user.id && activity.type === 4 ? ( + { + setDeleteIndex({ index, isDraft: false }); + }} + showEditButtons={true} + gameType={activity.subType ?? 0} + /> + ) : null, + ) + ) : ( +

Vous n'avez pas de jeux publiés.

+ )}

Mes activités publiées

{hasNoPublishedActivities ? (

Vous n'avez pas d'activités publiées.

) : ( activities.map((activity, index) => - user && activity.userId === user.id && !isMascotte(activity) ? ( + user && activity.userId === user.id && !isMascotte(activity) && activity.type !== 4 ? ( { } const response = await axiosRequest({ method: 'GET', - url: `/games/ableToPlay${serializeToQueryUrl({ + url: `/games/ableToPlayStandardGame${serializeToQueryUrl({ villageId: village.id, type, })}`, @@ -115,15 +115,15 @@ export const useGameRequests = () => { } const response = await axiosRequest({ method: 'GET', - url: `/games/ableToPlay${serializeToQueryUrl({ - type, + url: `/games/ableToPlayStandardGame${serializeToQueryUrl({ + subType: type, villageId: village.id, })}`, }); if (response.error) { return [] as Array; } - return response.data.games as Array; + return response.data.activities as Array; }, [village], ); @@ -146,7 +146,7 @@ export const useGameRequests = () => { const response = await axiosRequest({ method: 'GET', - url: `/games/play${serializeToQueryUrl({ + url: `/games/playStandard${serializeToQueryUrl({ villageId: village.id, type, })}`, @@ -170,11 +170,11 @@ export const useGameRequests = () => { * */ - const sendNewGameResponse = useCallback(async (id: number, value: string) => { + const sendNewGameResponse = useCallback(async (id: number, value: string, villageId: number) => { const response = await axiosRequest({ method: 'PUT', url: `/games/play/${id}`, - data: { value }, + data: { value, villageId }, }); if (response.error) { return false; diff --git a/src/svg/jeu/expression.svg b/src/svg/jeu/expression.svg new file mode 100644 index 000000000..35b9e5eac --- /dev/null +++ b/src/svg/jeu/expression.svg @@ -0,0 +1,3 @@ + + + diff --git a/types/game.type.ts b/types/game.type.ts index 8f0a1b1c8..17e5bb650 100644 --- a/types/game.type.ts +++ b/types/game.type.ts @@ -3,6 +3,52 @@ import type { Activity } from './activity.type'; export enum GameType { MIMIC = 0, MONEY = 1, + EXPRESSION = 2, +} + +export enum InputTypeEnum { + INPUT = 0, + RADIO = 1, + SELECT = 2, + IMAGE = 3, + VIDEO = 4, +} + +export type hiddenType = { + id: number; + value: string; +}; + +export type inputType = { + id: number; + type: InputTypeEnum; + values?: string[]; + label?: string; + response?: boolean; + isDisplayedInRecap?: boolean; + placeHolder?: string; + methodType?: methodType; + selectedValue?: string; + hidden?: hiddenType; + required?: boolean; + isIndice?: boolean; +}; + +export type StepsTypes = { + title?: string; + description?: string; + inputs?: inputType[]; +}; + +export type GameFieldConfigType = { + [type in GameType]: { + steps: Array; + }; +}; + +export enum methodType { + LANGUE = 'language', + CURRENCY = 'currency', } // export interface Game @@ -44,6 +90,7 @@ export type MimicsData = { game1: MimicData; game2: MimicData; game3: MimicData; + labelPresentation?: string; }; // --- three different objects --- @@ -51,6 +98,16 @@ export type MoneysData = { game1: MoneyData; game2: MoneyData; game3: MoneyData; + labelPresentation?: string; +}; + +// --- three differents expressions --- +export type ExpressionsData = { + langage: string | null; + game1: ExpressionData; + game2: ExpressionData; + game3: ExpressionData; + labelPresentation?: string; }; // --- structure of each mimique --- @@ -64,17 +121,58 @@ export type MimicData = { video: string | null; }; -// --- Money game three objects & money game structure --- +// --- structure of each expression game --- +export type ExpressionData = { + gameId: number | null; + createDate: string | Date | null; + origine?: string | null; + signification: string | null; + fakeSignification1: string | null; + fakeSignification2: string | null; + video: string | null; +}; export type MoneyData = { gameId: number | null; - name: string | null; - price: string | null; - description: string | null; - image: string | null; + createDate: string | Date | null; + origine?: string | null; + signification: string | null; + fakeSignification1: string | null; + fakeSignification2: string | null; + video: string | null; +}; + +// --- structure to send to the server --- +// --- On utilise ce type et pas ce au dessus --- +export type GameDataStep = { + game: StepsTypes[]; + language?: string; + monney?: string; + labelPresentation: string; + radio?: string; +}; + +export type GameDataMonneyOrExpression = { + userId: number; + villageId: number; + type: number; + subType: number; + game1: GameDataStep; + game2: GameDataStep; + game3: GameDataStep; + selectedPhase: number; +}; +export type DataForPlayed = { + game: StepsTypes; + labelPresentation: string; + language?: string; + monnaie?: string; + radio?: string; }; export type GameMimicActivity = Activity; export type GameMoneyActivity = Activity; -export type GameActivity = GameMimicActivity | GameMoneyActivity; +export type GameExpressionActivity = Activity; + +export type GameActivity = GameMimicActivity | GameMoneyActivity | GameExpressionActivity;