From 3f408f2390248c7e164144c5169cdd34216fa39c Mon Sep 17 00:00:00 2001 From: tmalahie Date: Sat, 8 Jan 2022 15:18:40 +0100 Subject: [PATCH] Finish implementing creations list page --- creations.php | 2 + v2/app/hooks/useEffectUpdate.tsx | 15 ++ v2/app/hooks/useFormSubmit.tsx | 14 +- v2/app/hooks/useSmoothFetch.ts | 6 +- v2/app/pages/creations.tsx | 191 +++++++++++++++++++++----- v2/app/styles/Creations.module.scss | 14 ++ v2/php-api/endpoints/getCreations.php | 39 +++--- 7 files changed, 220 insertions(+), 61 deletions(-) create mode 100644 v2/app/hooks/useEffectUpdate.tsx diff --git a/creations.php b/creations.php index b2341320..d413c2bf 100755 --- a/creations.php +++ b/creations.php @@ -1,4 +1,6 @@ { + const didMount = useRef(false); + + useEffect(() => { + if (!didMount.current) { + didMount.current = true; + return; + } + callback(dependencies); + }, dependencies); +}; + +export default useEffectOnUpdate; \ No newline at end of file diff --git a/v2/app/hooks/useFormSubmit.tsx b/v2/app/hooks/useFormSubmit.tsx index f3ef1ff7..ce4b649d 100644 --- a/v2/app/hooks/useFormSubmit.tsx +++ b/v2/app/hooks/useFormSubmit.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { NextRouter, useRouter } from "next/router"; import { FormEvent, useCallback } from "react"; function toJson(formData: FormData) { @@ -9,16 +9,20 @@ function toJson(formData: FormData) { return object; } +export function doSubmit(router: NextRouter, form: HTMLFormElement) { + router.push({ + pathname: form.action, + query: toJson(new FormData(form)) + }); +} + function useFormSubmit() { const router = useRouter(); return useCallback((e: FormEvent) => { const form = e.target; if (form instanceof HTMLFormElement) { e.preventDefault(); - router.push({ - pathname: form.action, - query: toJson(new FormData(form)) - }); + doSubmit(router, form); } }, [router]); } diff --git a/v2/app/hooks/useSmoothFetch.ts b/v2/app/hooks/useSmoothFetch.ts index 030dcb64..0a6e89a3 100644 --- a/v2/app/hooks/useSmoothFetch.ts +++ b/v2/app/hooks/useSmoothFetch.ts @@ -10,7 +10,8 @@ type SmoothParams = { requestOptions?: RequestInit; reloadDeps?: any[], cacheKey?: string, - disabled?: boolean + disabled?: boolean, + onSuccess?: (data: T) => void; } let seed = 1; @@ -77,7 +78,7 @@ type CacheHandler = Record void> }> const cacheHandler: CacheHandler = {} -function useSmoothFetch(input: RequestInfo, { placeholder, retryDelay = 1000, retryDelayMultiplier = 2, retryCount = Infinity, requestOptions, reloadDeps = [], cacheKey, disabled }: SmoothParams = {}) { +function useSmoothFetch(input: RequestInfo, { placeholder, retryDelay = 1000, retryDelayMultiplier = 2, retryCount = Infinity, requestOptions, reloadDeps = [], cacheKey, disabled, onSuccess }: SmoothParams = {}) { const placeholderVal = useMemo(() => { if (!disabled && cacheKey && cacheHandler[cacheKey]) { const { state } = cacheHandler[cacheKey]; @@ -127,6 +128,7 @@ function useSmoothFetch(input: RequestInfo, { placeholder, retryDelay = 1000, setAllStates({ data, loading: false, error: null }) if (cacheKey) cacheHandler[cacheKey].setStates.length = 0; + onSuccess?.(data); }) .catch(error => { if (currentRetryCount < retryCount) { diff --git a/v2/app/pages/creations.tsx b/v2/app/pages/creations.tsx index afb7557d..30aaf372 100644 --- a/v2/app/pages/creations.tsx +++ b/v2/app/pages/creations.tsx @@ -7,35 +7,94 @@ import Ad from "../components/Ad/Ad"; import WithAppContext from "../components/WithAppContext/WithAppContext"; import useSmoothFetch, { Placeholder } from "../hooks/useSmoothFetch"; import Skeleton from "../components/Skeleton/Skeleton"; -import { useMemo } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/router"; -import TrackCreationCard from "../components/TrackCreationCard/TrackCreationCard"; +import TrackCreationCard, { TrackCreation } from "../components/TrackCreationCard/TrackCreationCard"; import { buildQuery } from "../helpers/uris"; import Link from "next/link"; import useCreations from "../hooks/useCreations"; +import useFormSubmit, { doSubmit } from "../hooks/useFormSubmit"; +import useEffectOnUpdate from "../hooks/useEffectUpdate"; +const resPerPage = 60, resPerRow = 5; const CreationsList: NextPage = () => { const language = useLanguage(); const router = useRouter(); + const handleSearch = useFormSubmit(); + const { user, admin, tri, type, nom, auteur } = router.query; const { data: creator } = useSmoothFetch<{ name: string }>(`/api/user/${user}`, { disabled: !user }); const nTri = +tri || 0; - const singleType = (type !== ''); - const sortTabs = language ? ['By latest', 'Top rated', 'Trending'] : ['Les plus récents', 'Les mieux notés', 'Tendances']; - const types = language + const sortTabs = useMemo(() => language ? ['By latest', 'Top rated', 'Trending'] : ['Les plus récents', 'Les mieux notés', 'Tendances'], [language]); + const sortTabsData = useMemo(() => { + return sortTabs.map((sortTab, i) => ({ + text: sortTab, + url: "/creations?" + buildQuery({ + ...router.query, + tri: i + }) + })); + }, [sortTabs, router.query]); + const types = useMemo(() => language ? ['Complete mode - multicups', 'Quick mode - multicups', 'Complete mode - cups', 'Quick mode - cups', 'Complete mode - circuits', 'Quick mode - circuits', 'Complete mode - arenas', 'Quick mode - arenas'] : ['Mode complet - multicoupes', 'Mode simplifié - multicoupes', 'Mode complet - coupes', 'Mode simplifié - coupes', 'Mode complet - circuits', 'Mode simplifié - circuits', 'Mode complet - arènes', 'Mode simplifié - arènes'] + , [language]); + + const [page, setPage] = useState(1); const creationParams = useMemo(() => { const params = { - user, admin, tri, type, nom, auteur + user, admin, tri, type, nom, auteur, page }; return buildQuery(params); - }, [router.query]); + }, [router.query, page]); + useEffectOnUpdate(() => { + resetAbortController(); + setCreationsListHeights({ + current: 0, + max: 0 + }); + setPage(1); + }, [buildQuery(router.query)]); + useEffectOnUpdate(() => { + setQueryId(queryId + 1); + }, [creationParams]); + let [cardHeight, setCardHeight] = useState(0); + let [chunkHeight, setChunkHeight] = useState(0); + function getCardHeight() { + try { + if (window.matchMedia('(max-width: 800px)').matches) { + return 126; + } + } + catch (e) { + } + return 146; + } + useEffect(() => { + setCardHeight(cardHeight = getCardHeight()); + setChunkHeight(chunkHeight = cardHeight * resPerPage / resPerRow); + }, []); + const [creationsListHeights, setCreationsListHeights] = useState({ + current: 0, + max: 0 + }); + + function createAbortController() { + if (typeof AbortController !== "undefined") + return new AbortController(); + } + const controller = useRef(createAbortController()); + const [queryId, setQueryId] = useState(0); + function resetAbortController() { + controller.current?.abort(); + controller.current = createAbortController(); + } + const { data: creationsPayload, loading: creationsLoading } = useSmoothFetch(`/api/getCreations.php?${creationParams}`, { placeholder: () => ({ - data: Placeholder.array(60, (id) => ({ + data: Placeholder.array(resPerPage, (id) => ({ id, author: "", cicon: "", @@ -50,32 +109,90 @@ const CreationsList: NextPage = () => { })), count: 0, countByType: [] - }) + }), + onSuccess: (payload) => { + if (page === 1) + setCreationsList(payload.data); + else + setCreationsList([...creationsList, ...payload.data]); + setTimeout(() => { + const $creationsList = document.getElementById(styles.creationsList); + if ($creationsList) { + const $creationsListWrapper = $creationsList.parentNode; + if ($creationsListWrapper instanceof HTMLElement) { + if (!$creationsListWrapper.style.width) + $creationsListWrapper.style.width = $creationsListWrapper.offsetWidth + "px"; + } + setCreationsListHeights({ + current: Math.min(creationsListHeights.current + chunkHeight, $creationsList.scrollHeight), + max: $creationsList.scrollHeight + }); + setCreationsRendering(false); + } + }); + }, + reloadDeps: [queryId], + requestOptions: { + signal: controller.current?.signal + } }); - const creationCount = useMemo(() => { + const [creationsList, setCreationsList] = useState(creationsPayload.data); + const [creationCount, setCreationCount] = useState<{ total: number, byType: number[], isTotal: boolean }>(); + useEffect(() => { if (creationsLoading) return; - return { + setCreationCount({ total: creationsPayload.count, - byType: creationsPayload.countByType - } - }, [creationsPayload, creationsLoading]); + byType: creationsPayload.countByType, + isTotal: (creationsPayload.countByType.length > 1) + }) + }, [creationsLoading, creationsPayload]); + const [creationsRendering, setCreationsRendering] = useState(creationsLoading); + useEffect(() => { + if (creationsLoading) + setCreationsRendering(true); + }, [creationsLoading]); const { previewCreation } = useCreations(); - function defile() { + const creationsListStyle = useMemo(() => ({ + height: creationsListHeights.current, + minHeight: creationsRendering ? chunkHeight : Math.min(chunkHeight, creationsListHeights.max), + }), [creationsListHeights, chunkHeight, creationsRendering]); + + const lastPage = useMemo(() => (page * resPerPage >= creationCount?.total), [page, creationCount]); + function loadMore() { + const nextHeight = creationsListHeights.current + chunkHeight; + if ((nextHeight > creationsListHeights.max) && !lastPage) { + resetAbortController(); + setPage(page + 1); + } + else { + setCreationsListHeights({ + ...creationsListHeights, + current: Math.min(nextHeight, creationsListHeights.max) + }); + } } - function masque() { + function showLess() { + setCreationsListHeights({ + ...creationsListHeights, + current: creationsListHeights.current - chunkHeight + }); } function reduceAll() { - } - function handleTabSelect(e) { - e.preventDefault(); + setCreationsListHeights({ + ...creationsListHeights, + current: chunkHeight + }); } function scrollToTop(e) { e.preventDefault(); window.scrollTo(0, 0); } + if (!creationsLoading) + console.log(creationsList[0]?.id); + return ( @@ -89,23 +206,23 @@ const CreationsList: NextPage = () => { : <>Bienvenue dans la liste des circuits et arènes partagés par la communauté de Mario Kart PC !
Vous aussi, partagez les circuits que vous créez en cliquant sur "Partager le circuit" en bas à gauche de la page du circuit. )}

-
+
- {sortTabs.map((sortTab, i) => { - return (i === nTri) ? {sortTab} : {sortTab} + {sortTabsData.map((sortTab, i) => { + return (i === nTri) ? {sortTab.text} : {sortTab.text} })}
{language ? 'Creation type' : 'Type de création '}{": "} - doSubmit(router, e.target.form)}> + + {types.map((iType, i) => )}
{language ? 'Search' : 'Recherche '}{": "} {admin && } {" "} - {" "} + {" "}
@@ -113,21 +230,27 @@ const CreationsList: NextPage = () => {
- - {creationsPayload.data.map((creation) => )} - +
+ + {creationsList.map((creation, i) => )} + +
-

- defile()} />{"   "} - masque()} />{"   "} - reduceAll()} /> +

+ = creationsListHeights.max) && lastPage) })} value={language ? 'More' : 'Plus'} onClick={() => loadMore()} />{"   "} + = creationsListHeights.current })} value={language ? 'Less' : 'Moins'} onClick={() => showLess()} />{"   "} + = creationsListHeights.current })} value={language ? 'Minimize' : 'Réduire'} onClick={() => reduceAll()} />

+ {!creationsLoading && !creationsList.length &&

+ {language ? 'No result for this search' : 'Aucun résultat pour cette recherche'} +

} +

{language ? 'Back to top' : 'Retour haut de page'}{" - "} {language ? 'Back to Mario Kart PC' : 'Retour à Mario Kart PC'}

-
+ ); } diff --git a/v2/app/styles/Creations.module.scss b/v2/app/styles/Creations.module.scss index efdda158..b0337967 100644 --- a/v2/app/styles/Creations.module.scss +++ b/v2/app/styles/Creations.module.scss @@ -97,6 +97,9 @@ } .subbuttons { margin-top: 12px; + &.invisible, .invisible { + visibility: hidden; + } } @media screen and (min-width: 880px) { h2, .subbuttons { @@ -139,5 +142,16 @@ margin-right: auto; text-align: center; line-height: 0; + height: 0px; + overflow-x: auto; + overflow-y: hidden; + transition: height 1s; + } + #creationsList { + display: block; + } + h4 { + text-align: center; + margin-top: 20px; } } \ No newline at end of file diff --git a/v2/php-api/endpoints/getCreations.php b/v2/php-api/endpoints/getCreations.php index 0c54ce55..74eb2fe3 100644 --- a/v2/php-api/endpoints/getCreations.php +++ b/v2/php-api/endpoints/getCreations.php @@ -2,18 +2,17 @@ include('../includes/initdb.php'); require_once('../includes/creations.php'); require_once('../includes/api.php'); -$tri = isset($_GET['tri']) ? $_GET['tri']:0; -$type = isset($_GET['type']) ? $_GET['type']:''; -$nom = isset($_GET['nom']) ? stripslashes($_GET['nom']):''; -$auteur = isset($_GET['auteur']) ? stripslashes($_GET['auteur']):''; +$tri = isset($_GET['tri']) && is_numeric($_GET['tri']) ? $_GET['tri'] : 0; +$type = isset($_GET['type']) ? $_GET['type'] : ''; +$nom = isset($_GET['nom']) ? stripslashes($_GET['nom']) : ''; +$auteur = isset($_GET['auteur']) ? stripslashes($_GET['auteur']) : ''; $pids = null; if (isset($_GET['user'])) { - $user = $_GET['user']; - if ($getProfile = mysql_fetch_array(mysql_query('SELECT identifiant,identifiant2,identifiant3,identifiant4 FROM `mkprofiles` WHERE id="'. $user .'"'))) - $pids = array($getProfile['identifiant'],$getProfile['identifiant2'],$getProfile['identifiant3'],$getProfile['identifiant4']); -} -else - $user = ''; + $user = $_GET['user']; + if ($getProfile = mysql_fetch_array(mysql_query('SELECT identifiant,identifiant2,identifiant3,identifiant4 FROM `mkprofiles` WHERE id="' . $user . '"'))) + $pids = array($getProfile['identifiant'], $getProfile['identifiant2'], $getProfile['identifiant3'], $getProfile['identifiant4']); +} else + $user = ''; $singleType = ($type !== ''); if ($singleType) { $aCircuits = array($aCircuits[$type]); @@ -28,22 +27,22 @@ if ($nbCircuits > $MAX_CIRCUITS) $nbCircuits = $MAX_CIRCUITS; $aParams = array( - 'type' => $type, - 'tri' => $tri, - 'nom' => $nom, - 'auteur' => $auteur, - 'pids' => $pids, - 'max_circuits' => $nbCircuits, + 'type' => $type, + 'tri' => $tri, + 'nom' => $nom, + 'auteur' => $auteur, + 'pids' => $pids, + 'max_circuits' => $nbCircuits, ); -$page = isset($_GET['page']) ? $_GET['page']:1; +$page = isset($_GET['page']) ? $_GET['page'] : 1; if ($nbByType === null) - $nbByType = countTracksByType($aCircuits,$aParams); -$creationsList = listCreations($page,$nbByType,$weightsByType,$aCircuits,$aParams); + $nbByType = countTracksByType($aCircuits, $aParams); +$creationsList = listCreations($page, $nbByType, $weightsByType, $aCircuits, $aParams); $data = array(); foreach ($creationsList as &$creation) { $item = array( 'id' => isset($creation['id']) ? +$creation['id'] : +$creation['ID'], - 'publicationDate' => strtotime($creation['publication_date'])*1000, + 'publicationDate' => strtotime($creation['publication_date']) * 1000, 'name' => $creation['nom'], 'author' => $creation['auteur'], 'rating' => +$creation['note'],