diff --git a/app/routes/challenges/index.tsx b/app/routes/challenges/index.tsx index 7a2f648..5130141 100644 --- a/app/routes/challenges/index.tsx +++ b/app/routes/challenges/index.tsx @@ -1,11 +1,13 @@ -import { CircularProgress, Container, Typography } from "@mui/material"; +import { CircularProgress, Container, Pagination as PaginationMui, Typography } from "@mui/material"; import { + ActionFunction, json, LoaderFunction, useCatch, useLoaderData, useOutletContext, + useSubmit, useTransition, } from "remix"; @@ -19,22 +21,60 @@ import { Challenge } from "~/models/Challenge"; import { requireAuth } from "~/services/authentication"; import ChallengeGrid from "~/components/challenge/grids/challengeGrid"; -import { getManyChallenge } from "~/services/challenges"; +import { getChallengeCount, getManyChallenge } from "~/services/challenges"; import { ContextData } from "~/root"; import { blue } from "@mui/material/colors"; +import { Pagination } from "~/utils/pagination"; type LoaderData = { challengeResponse?: { + data?: Pagination; error?: string; success?: string; - challenges?: Challenge[]; }; }; -async function loadChallenges(token: string) { - const { code, ...challengeResponse } = await getManyChallenge(token, 100, 0); +const rowPerPage = 100; - return json({ challengeResponse } as LoaderData, code); +async function loadChallenges(token: string, page: number = 0) { + const challengeResponse = await getManyChallenge(token, rowPerPage, page * rowPerPage); + const countResponse = await getChallengeCount(token); + + if (challengeResponse.error) { + return json( + { challengeResponse: { error: challengeResponse.error } } as LoaderData, + challengeResponse.code + ); + } + + if (countResponse.error) { + return json( + { challengeResponse: { error: countResponse.error } } as LoaderData, + countResponse.code + ); + } + + return json({ + challengeResponse: { + pagination: { + page: page, + count: countResponse.count, + items: challengeResponse.challenges + } + } + } as LoaderData, Math.max(challengeResponse.code || 200, countResponse.code || 200)); +} + +export const action: ActionFunction = async ({ request }) => { + const token = await requireAuth(request, "/challenges"); + const formData = await request.formData(); + const page = Number(formData.get("page")); + + if (isNaN(page)) { + return json({ challengeResponse: { error: "Bad form data: page must be a number" } }, 400) + } + + return await loadChallenges(token, page); } export const loader: LoaderFunction = async ({ request }) => { @@ -46,10 +86,16 @@ export const loader: LoaderFunction = async ({ request }) => { export default function Challenges() { const loaderData = useLoaderData(); - const { API_URL } = useOutletContext(); - const transition = useTransition(); + const submit = useSubmit(); + + const handleChangePage = async (event: React.ChangeEvent, value: number) => { + const formData = new FormData(); + //Page starts from 1 instead of 0 for MUI Pagination component + formData.append("page", (value - 1).toString()); + submit(formData); + }; return ( @@ -60,16 +106,16 @@ export default function Challenges() { {generateAlert( "info", loaderData.challengeResponse?.success && - (!loaderData.challengeResponse?.challenges || - loaderData.challengeResponse.challenges.length === 0) + (!loaderData.challengeResponse?.data?.items || + loaderData.challengeResponse?.data?.items.length === 0) ? "Il n'y a aucun challenge pour l'instant" : undefined )} - {loaderData.challengeResponse?.challenges && - loaderData.challengeResponse?.challenges.length !== 0 && ( + {loaderData.challengeResponse?.data?.items && + loaderData.challengeResponse?.data?.items.length !== 0 && ( )} {transition.state === "submitting" && ( @@ -84,6 +130,11 @@ export default function Challenges() { }} /> )} + ); } diff --git a/app/routes/goodies/index.tsx b/app/routes/goodies/index.tsx index bac50d4..47988bb 100644 --- a/app/routes/goodies/index.tsx +++ b/app/routes/goodies/index.tsx @@ -1,11 +1,13 @@ -import { CircularProgress, Container, Typography } from "@mui/material"; +import { CircularProgress, Container, Pagination as PaginationMui, Typography } from "@mui/material"; import { + ActionFunction, json, LoaderFunction, useCatch, useLoaderData, useOutletContext, + useSubmit, useTransition, } from "remix"; @@ -16,23 +18,65 @@ import { } from "~/utils/error"; import { requireAuth } from "~/services/authentication"; -import { getManyGoodies } from "~/services/goodies"; +import { getGoodiesCount, getManyGoodies } from "~/services/goodies"; import GoodiesGrid from "~/components/goodies/grids/goodiesGrid"; import { Goodies } from "~/models/Goodies"; import { ContextData } from "~/root"; import { blue } from "@mui/material/colors"; +import { Pagination } from "~/utils/pagination"; type LoaderData = { - goodiesResponse?: { error?: string; goodies?: Goodies[]; success?: string }; + goodiesResponse?: { + error?: string; + data?: Pagination; + success?: string + }; }; -async function loadGoodies(token: string) { - const { code, ...goodiesResponse } = await getManyGoodies(token, 100, 0); +const rowPerPage = 100; - return json({ goodiesResponse } as LoaderData, code); +async function loadGoodies(token: string, page: number = 0) { + const goodiesResponse = await getManyGoodies(token, rowPerPage, page * rowPerPage); + const countResponse = await getGoodiesCount(token); + + if (goodiesResponse.error) { + return json( + { goodiesResponse: { error: goodiesResponse.error } } as LoaderData, + goodiesResponse.code + ); + } + + if (countResponse.error) { + return json( + { goodiesResponse: { error: countResponse.error } } as LoaderData, + countResponse.code + ); + } + + return json({ + goodiesResponse: { + pagination: { + page: page, + count: countResponse.count, + items: goodiesResponse.goodies, + } + } + } as LoaderData, Math.max(goodiesResponse.code || 200, countResponse.code || 200)); +} + +export const action: ActionFunction = async ({ request }) => { + const token = await requireAuth(request, "/goodies"); + const formData = await request.formData(); + const page = Number(formData.get("page")); + + if (isNaN(page)) { + return json({ challengeResponse: { error: "Bad form data: page must be a number" } }, 400) + } + + return await loadGoodies(token, page); } -//Function that handle GET resuests +//Function that handle GET requests export const loader: LoaderFunction = async ({ request }) => { const token = await requireAuth(request, "/goodies"); @@ -41,10 +85,17 @@ export const loader: LoaderFunction = async ({ request }) => { export default function Shop() { const loaderData = useLoaderData(); - const { API_URL } = useOutletContext(); - const transition = useTransition(); + const submit = useSubmit(); + + const handleChangePage = async (event: React.ChangeEvent, value: number) => { + const formData = new FormData(); + //Page starts from 1 instead of 0 for MUI Pagination component + formData.append("page", (value - 1).toString()); + submit(formData); + }; + return ( @@ -54,16 +105,16 @@ export default function Shop() { {generateAlert( "info", loaderData.goodiesResponse?.success && - (!loaderData.goodiesResponse?.goodies || - loaderData.goodiesResponse.goodies.length === 0) + (!loaderData.goodiesResponse?.data?.items || + loaderData.goodiesResponse?.data?.items.length === 0) ? "Il n'y a pas de goodies pour l'instant" : undefined )} - {loaderData.goodiesResponse?.goodies && - loaderData.goodiesResponse.goodies.length !== 0 && ( + {loaderData.goodiesResponse?.data?.items && + loaderData.goodiesResponse?.data?.items.length !== 0 && ( )} {transition.state === "submitting" && ( @@ -78,6 +129,11 @@ export default function Shop() { }} /> )} + ); } diff --git a/app/routes/index.tsx b/app/routes/index.tsx index c4984c6..f3547cf 100644 --- a/app/routes/index.tsx +++ b/app/routes/index.tsx @@ -32,7 +32,9 @@ export default function Index() { { name: "L'encas", imgSrc: "/assets/images/partnerships/l_encas.png" }, { name: "Tropic Addict", imgSrc: "/assets/images/partnerships/tropic_addict.png" }, { name: "Le 5 by La Cabane", imgSrc: "/assets/images/partnerships/le_5_by_la_cabane.png" }, - { name: "Laser Quest Comedie", imgSrc: "/assets/images/partnerships/laser_quest_comedie.jpeg" } + { name: "Laser Quest Comedie", imgSrc: "/assets/images/partnerships/laser_quest_comedie.jpeg" }, + { name: "Cartapapa", imgSrc: "/assets/images/partnerships/cartapapa.jpeg" }, + { name: "Run'Up", imgSrc: "/assets/images/partnerships/run_up.png" }, ]} /> diff --git a/app/routes/soutiens/index.tsx b/app/routes/soutiens/index.tsx index b2291f5..532c6a2 100644 --- a/app/routes/soutiens/index.tsx +++ b/app/routes/soutiens/index.tsx @@ -44,7 +44,7 @@ export default function Soutiens() { width="560" style={{ border: "none", overflow: "hidden" }} scrolling="no" - allowFullScreen="true" + allowFullScreen={true} allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share" > diff --git a/app/routes/users/index.tsx b/app/routes/users/index.tsx index 2bb3515..c5fc221 100644 --- a/app/routes/users/index.tsx +++ b/app/routes/users/index.tsx @@ -1,11 +1,13 @@ -import { CircularProgress, Container, Typography } from "@mui/material"; +import { CircularProgress, Container, Pagination as PaginationMui, Typography } from "@mui/material"; import { + ActionFunction, json, LoaderFunction, useCatch, useLoaderData, useOutletContext, + useSubmit, useTransition, } from "remix"; @@ -18,35 +20,81 @@ import { import UserList from "~/components/user/userList"; import { requireAuth } from "~/services/authentication"; import { User } from "~/models/User"; -import { getManyUser } from "~/services/user"; +import { getManyUser, getUserCount } from "~/services/user"; import { ContextData } from "~/root"; import { blue } from "@mui/material/colors"; +import { Pagination } from "~/utils/pagination"; type LoaderData = { - userResponse?: { error?: string; users?: User[]; success?: string }; + userResponse?: { + error?: string; + data?: Pagination; + success?: string; + }; }; -async function loadUsers(token: string) { - const { code, ...userResponse } = await getManyUser(token, 1000); +const rowPerPage = 100; - console.log(userResponse.users); +async function loadUsers(token: string, page: number = 0) { + const userResponse = await getManyUser(token, rowPerPage, page * rowPerPage); + const countResponse = await getUserCount(token); - return json({ userResponse } as LoaderData, code); + if (userResponse.error) { + return json( + { userResponse: { error: userResponse.error } } as LoaderData, + userResponse.code + ); + } + + if (countResponse.error) { + return json( + { userResponse: { error: countResponse.error } } as LoaderData, + countResponse.code + ); + } + + return json({ + userResponse: { + pagination: { + page: page, + count: countResponse.count, + items: userResponse.users, + } + } + } as LoaderData, Math.max(userResponse.code || 200, countResponse.code || 200)); +} + +export const action: ActionFunction = async ({ request }) => { + const token = await requireAuth(request, "/users"); + const formData = await request.formData(); + const page = Number(formData.get("page")); + + if (isNaN(page)) { + return json({ challengeResponse: { error: "Bad form data: page must be a number" } }, 400) + } + + return await loadUsers(token, page); } //Function that handle GET resuests export const loader: LoaderFunction = async ({ request }) => { - const token = await requireAuth(request, "/goodies"); + const token = await requireAuth(request, "/users"); return await loadUsers(token); }; export default function Users() { const loaderData = useLoaderData(); - const { API_URL } = useOutletContext(); - const transition = useTransition(); + const submit = useSubmit(); + + const handleChangePage = async (event: React.ChangeEvent, value: number) => { + const formData = new FormData(); + //Page starts from 1 instead of 0 for MUI Pagination component + formData.append("page", (value - 1).toString()); + submit(formData); + }; return ( @@ -57,17 +105,17 @@ export default function Users() { {generateAlert( "info", loaderData.userResponse?.success && - (!loaderData.userResponse?.users || - loaderData.userResponse?.users.length === 0) + (!loaderData.userResponse?.data?.items || + loaderData.userResponse?.data?.items.length === 0) ? "Il n'y a aucun utilisateur à afficher" : undefined )} - {loaderData.userResponse?.users && - loaderData.userResponse?.users.length !== 0 && ( + {loaderData.userResponse?.data?.items && + loaderData.userResponse?.data?.items.length !== 0 && (
b.totalEarnedPoints - a.totalEarnedPoints )} /> @@ -85,6 +133,11 @@ export default function Users() { }} /> )} + ); } diff --git a/app/services/accomplishment.ts b/app/services/accomplishment.ts index f681c65..9f481d1 100644 --- a/app/services/accomplishment.ts +++ b/app/services/accomplishment.ts @@ -307,3 +307,42 @@ export async function deleteProof(token: string, accomplishmentId: number) { }; } } + +export async function getAccomplishmentCount( + token: string, + challengeId?: number, + userId?: number, + validation?: Validation +) { + const searchParams = buildSearchParams( + { key: "challengeId", val: challengeId?.toString() }, + { key: "userId", val: userId?.toString() }, + { key: "status", val: validation } + ); + try { + const reply = await axios.get<{ + message: string; + count: number; + }>(`/accomplishment/count${searchParams}`, { + headers: buildAxiosHeaders(token), + }); + + return { + success: reply.data.message, + code: reply.status, + count: reply.data.count, + }; + } catch (err) { + if ( + axios.isAxiosError(err) && + typeof err.response?.data.message === "string" + ) { + return { error: err.response.data.message, code: err.response.status }; + } + console.error(err); + return { + error: + "Sorry, it seems there is some problem reaching our API. Please contact and administrator.", + }; + } +} \ No newline at end of file diff --git a/app/services/challenges.ts b/app/services/challenges.ts index 2c2ec80..9db0af6 100644 --- a/app/services/challenges.ts +++ b/app/services/challenges.ts @@ -284,3 +284,34 @@ export async function deleteChallengePicture( }; } } + +export async function getChallengeCount( + token: string, +) { + try { + const reply = await axios.get<{ + message: string; + count: number; + }>("/challenge/count", { + headers: buildAxiosHeaders(token), + }); + + return { + success: reply.data.message, + code: reply.status, + count: reply.data.count, + }; + } catch (err) { + if ( + axios.isAxiosError(err) && + typeof err.response?.data.message === "string" + ) { + return { error: err.response.data.message, code: err.response.status }; + } + console.error(err); + return { + error: + "Sorry, it seems there is some problem reaching our API. Please contact and administrator.", + }; + } +} \ No newline at end of file diff --git a/app/services/goodies.ts b/app/services/goodies.ts index fb01a00..2c34056 100644 --- a/app/services/goodies.ts +++ b/app/services/goodies.ts @@ -199,8 +199,8 @@ export async function putGoodiesPicture( axios.isAxiosError(err) && typeof err.response?.data.message === "string" ) { - if(err.response.data.message.includes("request file too large")){ - return {error: "Fichier trop volumineux, veuiller envoyer un fichier plus léger"} + if (err.response.data.message.includes("request file too large")) { + return { error: "Fichier trop volumineux, veuiller envoyer un fichier plus léger" } } return { error: err.response.data.message, code: err.response.status }; } @@ -272,3 +272,34 @@ export async function deleteGoodiesPicture(token: string, goodiesId: number) { }; } } + +export async function getGoodiesCount( + token: string, +) { + try { + const reply = await axios.get<{ + message: string; + count: number; + }>("/goodies/count", { + headers: buildAxiosHeaders(token), + }); + + return { + success: reply.data.message, + code: reply.status, + count: reply.data.count, + }; + } catch (err) { + if ( + axios.isAxiosError(err) && + typeof err.response?.data.message === "string" + ) { + return { error: err.response.data.message, code: err.response.status }; + } + console.error(err); + return { + error: + "Sorry, it seems there is some problem reaching our API. Please contact and administrator.", + }; + } +} \ No newline at end of file diff --git a/app/services/purchase.ts b/app/services/purchase.ts index 6413d90..be2270a 100644 --- a/app/services/purchase.ts +++ b/app/services/purchase.ts @@ -179,3 +179,42 @@ export async function getPurchase(token: string, purchaseId: number) { }; } } + +export async function getPurchaseCount( + token: string, + goodiesId?: number, + userId?: number, + delivered?: boolean, +) { + const searchParams = buildSearchParams( + { key: "goodiesId", val: goodiesId?.toString() }, + { key: "userId", val: userId?.toString() }, + { key: "delivered", val: delivered?.toString() } + ); + try { + const reply = await axios.get<{ + message: string; + count: number; + }>(`/purchase/count${searchParams}`, { + headers: buildAxiosHeaders(token), + }); + + return { + success: reply.data.message, + code: reply.status, + count: reply.data.count, + }; + } catch (err) { + if ( + axios.isAxiosError(err) && + typeof err.response?.data.message === "string" + ) { + return { error: err.response.data.message, code: err.response.status }; + } + console.error(err); + return { + error: + "Sorry, it seems there is some problem reaching our API. Please contact and administrator.", + }; + } +} \ No newline at end of file diff --git a/app/services/user.ts b/app/services/user.ts index 8e06136..7cec6cc 100644 --- a/app/services/user.ts +++ b/app/services/user.ts @@ -262,8 +262,8 @@ export async function putAvatar(token: string, userId: number, avatar: Blob) { axios.isAxiosError(err) && typeof err.response?.data.message === "string" ) { - if(err.response.data.message.includes("request file too large")){ - return {error: "Fichier trop volumineux, veuiller envoyer un fichier plus léger"} + if (err.response.data.message.includes("request file too large")) { + return { error: "Fichier trop volumineux, veuiller envoyer un fichier plus léger" } } return { error: err.response.data.message, code: err.response.status }; } @@ -335,3 +335,34 @@ export async function deleteAvatar(token: string, userId: number) { }; } } + +export async function getUserCount( + token: string, +) { + try { + const reply = await axios.get<{ + message: string; + count: number; + }>("/purchase/count", { + headers: buildAxiosHeaders(token), + }); + + return { + success: reply.data.message, + code: reply.status, + count: reply.data.count, + }; + } catch (err) { + if ( + axios.isAxiosError(err) && + typeof err.response?.data.message === "string" + ) { + return { error: err.response.data.message, code: err.response.status }; + } + console.error(err); + return { + error: + "Sorry, it seems there is some problem reaching our API. Please contact and administrator.", + }; + } +} \ No newline at end of file diff --git a/app/utils/pagination.tsx b/app/utils/pagination.tsx new file mode 100644 index 0000000..0e2d3fd --- /dev/null +++ b/app/utils/pagination.tsx @@ -0,0 +1,5 @@ +export type Pagination = { + page: number, + count: number, + items: T[], +} \ No newline at end of file diff --git a/public/assets/images/partnerships/cartapapa.jpeg b/public/assets/images/partnerships/cartapapa.jpeg new file mode 100644 index 0000000..accd72f Binary files /dev/null and b/public/assets/images/partnerships/cartapapa.jpeg differ