diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 917c71888..198783d2a 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -11,6 +11,7 @@ import featureFlagController from './featureFlag'; import { gameController } from './game'; import { imageController } from './image'; import { languageController } from './languages'; +import { pelicoController } from './pelicoPresentation'; import { storyController } from './story'; import { studentController } from './student'; import { teacherController } from './teacher'; @@ -41,6 +42,7 @@ const controllers = [ teacherController, studentController, featureFlagController, + pelicoController, ]; for (let i = 0, n = controllers.length; i < n; i++) { diff --git a/server/controllers/pelicoPresentation.ts b/server/controllers/pelicoPresentation.ts new file mode 100644 index 000000000..5cd12686b --- /dev/null +++ b/server/controllers/pelicoPresentation.ts @@ -0,0 +1,114 @@ +import type { JSONSchemaType } from 'ajv'; +import type { NextFunction, Request, Response } from 'express'; + +import type { ActivityContent } from '../../types/activity.type'; +import { PelicoPresentation } from '../entities/pelicoPresentation'; +import { UserType } from '../entities/user'; +import { AppDataSource } from '../utils/data-source'; +import { ajv, sendInvalidDataError } from '../utils/jsonSchemaValidator'; +import { Controller } from './controller'; + +const pelicoController = new Controller('/pelico-presentation'); // Défini le préfixe de route + +// --- Récupérer toutes les présentations Pelico --- +pelicoController.get({ path: '', userType: UserType.OBSERVATOR }, async (_req: Request, res: Response) => { + const presentations = await AppDataSource.getRepository(PelicoPresentation).find(); + res.sendJSON(presentations); +}); + +// --- Récupérer une présentation Pelico --- +pelicoController.get({ path: '/:id', userType: UserType.OBSERVATOR }, async (req: Request, res: Response) => { + const id = parseInt(req.params.id, 10) || 1; + const presentation = await AppDataSource.getRepository(PelicoPresentation).findOne({ where: { id } }); + res.sendJSON(presentation); +}); + +// --- Créer une présentation Pelico --- +type CreatePelicoData = { + content: ActivityContent[]; +}; +const CREATE_SCHEMA: JSONSchemaType = { + type: 'object', + properties: { + content: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'number', nullable: false }, + type: { type: 'string', nullable: false, enum: ['text', 'video', 'image', 'h5p', 'sound', 'document'] }, + value: { type: 'string', nullable: false }, + }, + required: ['type', 'value'], + }, + nullable: false, + }, + }, + required: ['content'], + additionalProperties: false, +}; +const createValidator = ajv.compile(CREATE_SCHEMA); +pelicoController.post({ path: '', userType: UserType.ADMIN }, async (req: Request, res: Response) => { + const data = req.body; + if (!createValidator(data)) { + sendInvalidDataError(createValidator); + return; + } + const presentation = new PelicoPresentation(); + presentation.content = data.content; + await AppDataSource.getRepository(PelicoPresentation).save(presentation); + res.sendJSON(presentation); +}); + +// --- Mettre à jour une présentation Pelico --- +type UpdatePelicoData = { + content?: ActivityContent[]; +}; +const UPDATE_SCHEMA: JSONSchemaType = { + type: 'object', + properties: { + content: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'number', nullable: false }, + type: { type: 'string', nullable: false, enum: ['text', 'video', 'image', 'h5p', 'sound', 'document'] }, + value: { type: 'string', nullable: false }, + }, + required: ['type', 'value'], + }, + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; +const updateValidator = ajv.compile(UPDATE_SCHEMA); +pelicoController.put({ path: '/:id', userType: UserType.ADMIN }, async (req: Request, res: Response, next: NextFunction) => { + const data = req.body; + if (!updateValidator(data)) { + sendInvalidDataError(updateValidator); + return; + } + const id = parseInt(req.params.id, 10) || 0; + const presentation = await AppDataSource.getRepository(PelicoPresentation).findOne({ where: { id } }); + if (!presentation) { + next(); + return; + } + + presentation.content = data.content || presentation.content; + + await AppDataSource.getRepository(PelicoPresentation).save(presentation); + res.sendJSON(presentation); +}); + +// --- Supprimer une présentation Pelico --- +pelicoController.delete({ path: '/:id', userType: UserType.ADMIN }, async (req: Request, res: Response) => { + const id = parseInt(req.params.id, 10) || 0; + await AppDataSource.getRepository(PelicoPresentation).delete({ id }); + res.status(204).send(); +}); + +export { pelicoController }; diff --git a/server/entities/pelicoPresentation.ts b/server/entities/pelicoPresentation.ts new file mode 100644 index 000000000..6cb25f4be --- /dev/null +++ b/server/entities/pelicoPresentation.ts @@ -0,0 +1,12 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +import type { ActivityContent } from '../../types/activity.type'; + +@Entity() +export class PelicoPresentation { + @PrimaryGeneratedColumn() + id: number; + + @Column('json') + content: ActivityContent[]; +} diff --git a/server/migrations/1715874475677-CreateTablePelicoPresentation.ts b/server/migrations/1715874475677-CreateTablePelicoPresentation.ts new file mode 100644 index 000000000..e4cee0324 --- /dev/null +++ b/server/migrations/1715874475677-CreateTablePelicoPresentation.ts @@ -0,0 +1,29 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; +import { Table } from 'typeorm'; + +export class CreateTablePelicoPresentation1715874475677 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'pelico_presentation', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + generationStrategy: 'increment', + }, + { + name: 'content', + type: 'json', + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('pelico_presentation'); + } +} diff --git a/src/api/pelicoPresentation/pelicoPresentation.delete.ts b/src/api/pelicoPresentation/pelicoPresentation.delete.ts new file mode 100644 index 000000000..c81cc7cf3 --- /dev/null +++ b/src/api/pelicoPresentation/pelicoPresentation.delete.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; +import { useMutation, useQueryClient } from 'react-query'; + +const BASE_URL = '/api/pelico-presentation'; + +// Supprimer une présentation Pelico +export const useDeletePelicoPresentation = () => { + const queryClient = useQueryClient(); + + return useMutation( + async (id: number) => { + await axios.delete(`${BASE_URL}/${id}`); + }, + { + onSuccess: () => { + queryClient.invalidateQueries('pelicoPresentation'); + }, + }, + ); +}; diff --git a/src/api/pelicoPresentation/pelicoPresentation.get.ts b/src/api/pelicoPresentation/pelicoPresentation.get.ts new file mode 100644 index 000000000..c117336b0 --- /dev/null +++ b/src/api/pelicoPresentation/pelicoPresentation.get.ts @@ -0,0 +1,38 @@ +import axios from 'axios'; +import { useQuery } from 'react-query'; + +const BASE_URL = '/api/pelico-presentation'; + +// Récupérer une présentation Pelico spécifique +export const usePelicoPresentation = (id: number) => { + return useQuery(['pelicoPresentation', id], async () => { + const { data } = await axios.get(`${BASE_URL}/${id}`); + return data; + }); +}; + +// Récupérer toutes les présentations Pelico +export const usePelicoPresentatations = () => { + return useQuery('pelicoPresentatation', async () => { + const { data } = await axios.get(BASE_URL); + return data; + }); +}; + +// async function getPelicoPresentation(id: number) { +// const { data } = await axios.get(`${BASE_URL}/${id}`); +// return data; +// } + +// export const useGetPelicoPresentation = (id: number) => { +// return useQuery(['pelico-presentation'], getPelicoPresentation(id)); +// }; + +// async function getPelicoPresentations() { +// const { data } = await axios.get(`${BASE_URL}`); +// return data; +// } + +// export const useGetPelicoPresentations = () => { +// return useQuery(['pelico-presentation'], getPelicoPresentations); +// }; diff --git a/src/api/pelicoPresentation/pelicoPresentation.post.ts b/src/api/pelicoPresentation/pelicoPresentation.post.ts new file mode 100644 index 000000000..a30a5b9ab --- /dev/null +++ b/src/api/pelicoPresentation/pelicoPresentation.post.ts @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { useMutation, useQueryClient } from 'react-query'; + +import type { PelicoPresentationContent } from 'types/pelicoPresentation.type'; + +const BASE_URL = '/api/pelico-presentation'; + +// Créer une nouvelle présentation Pelico +export const useCreatePelicoPresentation = () => { + const queryClient = useQueryClient(); + + return useMutation( + async (content: PelicoPresentationContent[]) => { + const { data } = await axios.post(BASE_URL, { content }); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries('pelicoPresentation'); + }, + }, + ); +}; diff --git a/src/api/pelicoPresentation/pelicoPresentation.put.ts b/src/api/pelicoPresentation/pelicoPresentation.put.ts new file mode 100644 index 000000000..e7ff65fb8 --- /dev/null +++ b/src/api/pelicoPresentation/pelicoPresentation.put.ts @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { useMutation, useQueryClient } from 'react-query'; + +import type { PelicoPresentation } from 'types/pelicoPresentation.type'; + +const BASE_URL = '/api/pelico-presentation'; + +// Mettre à jour une présentation Pelico +export const useUpdatePelicoPresentation = () => { + const queryClient = useQueryClient(); + + return useMutation( + async ({ id, content }: PelicoPresentation) => { + const { data } = await axios.put(`${BASE_URL}/${id}`, { content }); + return data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries('pelicoPresentation'); + }, + }, + ); +}; diff --git a/src/pages/admin/newportal/manage/settings/pelico/index.tsx b/src/pages/admin/newportal/manage/settings/pelico/index.tsx index 8bed409b0..f06f6afbb 100644 --- a/src/pages/admin/newportal/manage/settings/pelico/index.tsx +++ b/src/pages/admin/newportal/manage/settings/pelico/index.tsx @@ -1,9 +1,111 @@ import Link from 'next/link'; -import React from 'react'; +import { useRouter } from 'next/router'; +import { useSnackbar } from 'notistack'; +import React, { useEffect, useState } from 'react'; +import { useQueryClient } from 'react-query'; +import Button from '@mui/material/Button'; + +import { usePelicoPresentation } from 'src/api/pelicoPresentation/pelicoPresentation.get'; +import { useCreatePelicoPresentation } from 'src/api/pelicoPresentation/pelicoPresentation.post'; +import { useUpdatePelicoPresentation } from 'src/api/pelicoPresentation/pelicoPresentation.put'; +import { ContentEditor } from 'src/components/activities/content'; +import { UserContext } from 'src/contexts/userContext'; import BackArrow from 'src/svg/back-arrow.svg'; +import type { PelicoPresentation, PelicoPresentationContent, PelicoPresentationContentType } from 'types/pelicoPresentation.type'; +import { UserType } from 'types/user.type'; const Pelico = () => { + const queryClient = useQueryClient(); + const router = useRouter(); + const { user } = React.useContext(UserContext); + const hasAccess = user !== null && user.type in [UserType.MEDIATOR, UserType.ADMIN, UserType.SUPER_ADMIN]; + const [presentation, setPresentation] = useState({ content: [], id: 1 }); + const { enqueueSnackbar } = useSnackbar(); + + const { data: presentationData, isLoading: presentationLoading, isSuccess: presentationSuccess } = usePelicoPresentation(1); + const { mutate: createPresentation, isLoading: createLoading, isSuccess: createSuccess, isError: createError } = useCreatePelicoPresentation(); + const { mutate: updatePresentation, isLoading: updateLoading, isSuccess: updateSuccess, isError: updateError } = useUpdatePelicoPresentation(); + + useEffect(() => { + if (presentationSuccess) { + if (presentationData !== null) { + setPresentation(presentationData); + } + } + if (createSuccess || updateSuccess) { + queryClient.invalidateQueries({ queryKey: ['activities'] }); + enqueueSnackbar('Modifications enregistrées !', { + variant: 'success', + }); + router.push('/admin/newportal/manage/settings'); + } + if (createError || updateError) { + enqueueSnackbar("Une erreur s'est produite lors de la modifications !", { + variant: 'error', + }); + router.push('/admin/newportal/manage/settings'); + } + }, [presentationSuccess, createSuccess, updateSuccess, createError, updateError, queryClient, enqueueSnackbar, presentationData, router]); + + if (!hasAccess) { + return

Vous n'avez pas accès à cette page, vous devez être médiateur, modérateur ou super admin.

; + } + if (createLoading || presentationLoading || updateLoading) { + return
Loading...
; + } + + const updateContent = (content: PelicoPresentationContent[]) => { + if (!presentation) return; + updateActivity({ content: content }); + }; + + const addContent = (type: PelicoPresentationContentType, value: string = '', index?: number) => { + if (!presentation) { + return; + } + const newContent = presentation.content ? [...presentation.content] : []; + const newId = Math.max(1, ...newContent.map((p) => p.id)) + 1; + if (index !== undefined) { + newContent.splice(index, 0, { + id: newId, + type, + value, + }); + } else { + newContent.push({ + id: newId, + type, + value, + }); + } + updateActivity({ content: newContent }); + }; + + const deleteContent = (index: number) => { + if (!presentation) { + return; + } + const newContent = presentation.content ? [...presentation.content] : []; + if (newContent.length <= index) { + return; + } + newContent.splice(index, 1); + updateActivity({ content: newContent }); + }; + + const updateActivity = (updatedContent: { content: PelicoPresentationContent[] }) => { + setPresentation((prevState: PelicoPresentation) => (prevState === null ? prevState : { ...prevState, ...updatedContent })); + }; + + const onSave = () => { + if (presentationData == null) { + createPresentation(presentation.content); + } else { + updatePresentation(presentation); + } + }; + return (
@@ -12,6 +114,12 @@ const Pelico = () => {

Présentation de Pélico

+ +
+ +
); }; diff --git a/src/pages/pelico-profil.tsx b/src/pages/pelico-profil.tsx index dde6bacca..458f2d85d 100644 --- a/src/pages/pelico-profil.tsx +++ b/src/pages/pelico-profil.tsx @@ -1,15 +1,39 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Box } from '@mui/material'; +import { usePelicoPresentation } from 'src/api/pelicoPresentation/pelicoPresentation.get'; import { Base } from 'src/components/Base'; import { PelicoProfilNavigation } from 'src/components/accueil/PelicoProfilNavigation'; +import { ContentView } from 'src/components/activities/content/ContentView'; import { UserContext } from 'src/contexts/userContext'; import { primaryColor } from 'src/styles/variables.const'; +import type { PelicoPresentation } from 'types/pelicoPresentation.type'; -// const PelicoProfil = () => { const { user } = React.useContext(UserContext); + const [presentation, setPresentation] = useState({ content: [], id: 1 }); + const { + data: presentationData, + isLoading: presentationLoading, + isError: presentationError, + isSuccess: presentationSuccess, + } = usePelicoPresentation(1); // Récupère la présentation avec l'id 1 + + useEffect(() => { + if (presentationSuccess) { + if (presentationData !== null) { + setPresentation(presentationData); + } + } + }, [presentationSuccess, presentationData]); + + if (presentationLoading) { + return
Loading...
; + } + if (presentationError) { + return

Error!

; + } return ( <> @@ -22,31 +46,7 @@ const PelicoProfil = () => { }} >

Pelico, la mascotte d'1Village, se présente

-
-

- Bonjour les Pélicopains, -
-
- Je suis Pélico, un toucan qui adore voyager ! Cette année, nous allons échanger tous ensemble sur 1Village. Mes amies et moi serons là - toute l’année pour vous guider dans ce voyage. -
-
- Vous vous demandez certainement pourquoi je m’appelle Pélico… alors que je ne suis pas un pélican ? -
-
- Mon nom vient du mot espagnol “perico”, qui est une espèce de perroquet d’Amérique du Sud. Lorsque l’on voyage et que l’on tente - d’apprendre une langue comme moi, on peut vite se transformer en perroquet qui répète tout ce qu’il entend. Des amis m’ont donc nommé - ainsi au cours de l’un de mes voyages en Amérique du Sud.. Et comme le R de “perico” se prononce comme un L, voilà pourquoi on - m’appelle ainsi ! -
-
- Vous voyez, en se questionnant, on découvre de drôles d’anecdotes. -
-
- Adoptez la même posture avec vos amis, vos correspondants, votre famille et vous verrez que vous apprendrez plein de choses sur le - monde qui vous entoure ! -

-
+ )} diff --git a/types/pelicoPresentation.type.ts b/types/pelicoPresentation.type.ts new file mode 100644 index 000000000..a5ee6f53b --- /dev/null +++ b/types/pelicoPresentation.type.ts @@ -0,0 +1,12 @@ +export type PelicoPresentationContentType = 'text' | 'video' | 'image' | 'h5p' | 'sound' | 'document'; + +export interface PelicoPresentationContent { + id: number; + type: PelicoPresentationContentType; + value: string; +} + +export interface PelicoPresentation { + id: number; + content: PelicoPresentationContent[]; +}