frontend: FunctionsList et début de /admin
This commit is contained in:
		| @@ -1,5 +1,6 @@ | ||||
| import Link from 'next/link'; | ||||
| import { useState, forwardRef } from 'react'; | ||||
| import date from 'date-and-time'; | ||||
| import Loader from '../Loader'; | ||||
| import './FunctionCard.css'; | ||||
| import { API_URL } from '../../utils/config'; | ||||
| @@ -16,8 +17,18 @@ const FunctionCard = forwardRef((props, ref) => { | ||||
|  | ||||
|     return ( | ||||
|         <Link  | ||||
|             href={isFormOrArticle ? "/functions/[slug]" : `/functions/${props.slug}`}  | ||||
|             as={`/functions/${props.slug}`} | ||||
|             {  | ||||
|                 ...(props.isAdmin) ?  | ||||
|                 { | ||||
|                     href: "/admin/[slug]", | ||||
|                     as: `/admin/${props.slug}` | ||||
|                 }  | ||||
|                     :  | ||||
|                 { | ||||
|                     href: (isFormOrArticle) ? "/functions/[slug]" : `/functions/${props.slug}`, | ||||
|                     as: `/functions/${props.slug}` | ||||
|                 } | ||||
|             } | ||||
|         > | ||||
|             {/* FunctionCard a une hauteur pendant chargement */} | ||||
|             <div ref={ref} style={isLoading ? { height: "360px", justifyContent: "center" } : null} className={"FunctionCard col-sm-24 col-md-10 col-xl-7"}> | ||||
| @@ -31,8 +42,8 @@ const FunctionCard = forwardRef((props, ref) => { | ||||
|                         <p className="FunctionCard__description">{props.description}</p> | ||||
|                     </div> | ||||
|                     <div className="FunctionCard__info"> | ||||
|                         <p className="FunctionCard__category" style={{ backgroundColor: props.category.color }}>{props.category.name}</p> | ||||
|                         <p className="FunctionCard__publication-date">{props.publicationDate}</p> | ||||
|                         <p className="FunctionCard__category" style={{ backgroundColor: props.categorie.color }}>{props.categorie.name}</p> | ||||
|                         <p className="FunctionCard__publication-date">{date.format(new Date(props.createdAt), 'DD/MM/YYYY', true)}</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|   | ||||
							
								
								
									
										100
									
								
								website/components/FunctionsList/FunctionsList.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								website/components/FunctionsList/FunctionsList.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| import { useState, useEffect, useRef, useCallback } from 'react'; | ||||
| import { useRouter } from 'next/router'; | ||||
| import FunctionCard from '../FunctionCard/FunctionCard'; | ||||
| import Loader from '../Loader'; | ||||
| import api from '../../utils/api'; | ||||
| import useAPI from '../../hooks/useAPI'; | ||||
| import './FunctionsList.css'; | ||||
|  | ||||
| const FunctionsList = (props) => { | ||||
|  | ||||
|     const { categoryId } = useRouter().query; | ||||
|  | ||||
|     // State de recherche et de catégories | ||||
|     const [, categories]                = useAPI('/categories'); | ||||
|     const [inputSearch, setInputSearch] = useState({ search: "", selectedCategory: categoryId || "0" }); | ||||
|  | ||||
|     // State pour afficher les fonctions | ||||
|     const [functionsData, setFunctionsData]         = useState({ hasMore: true, rows: [] }); | ||||
|     const [isLoadingFunctions, setLoadingFunctions] = useState(true); | ||||
|     const [pageFunctions, setPageFunctions]         = useState(1); | ||||
|  | ||||
|     // Récupère la catégorie avec la query categoryId | ||||
|     useEffect(() => { | ||||
|         if (categoryId) { | ||||
|             handleChange({ target: { name: "selectedCategory", value: categoryId } }); | ||||
|         } | ||||
|     }, [categoryId]); | ||||
|  | ||||
|     // Récupère les fonctions si la page change | ||||
|     useEffect(() => { | ||||
|         getFunctionsData().then((data) => setFunctionsData({  | ||||
|             hasMore: data.hasMore,  | ||||
|             rows: [...functionsData.rows, ...data.rows]  | ||||
|         })); | ||||
|     }, [pageFunctions]); | ||||
|  | ||||
|     // Récupère les fonctions si la catégorie/recherche change | ||||
|     useEffect(() => { | ||||
|         getFunctionsData().then((data) => setFunctionsData(data)); | ||||
|     }, [inputSearch.selectedCategory, inputSearch.search]); | ||||
|  | ||||
|     // Permet la pagination au scroll | ||||
|     const observer = useRef(); | ||||
|     const lastFunctionCardRef = useCallback((node) => { | ||||
|         if (isLoadingFunctions) return; | ||||
|         if (observer.current) observer.current.disconnect(); | ||||
|         observer.current = new IntersectionObserver((entries) => { | ||||
|             if (entries[0].isIntersecting && functionsData.hasMore) { | ||||
|                 setPageFunctions(pageFunctions + 1); | ||||
|             } | ||||
|         }, { threshold: 1 }); | ||||
|         if (node) observer.current.observe(node); | ||||
|     }, [isLoadingFunctions, functionsData.hasMore]); | ||||
|  | ||||
|     const getFunctionsData = () => { | ||||
|         setLoadingFunctions(true); | ||||
|         return new Promise(async (next) => { | ||||
|             const result = await api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`); | ||||
|             setLoadingFunctions(false); | ||||
|             next(result.data); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     const handleChange = (event) => { | ||||
|         const inputSearchNew = { ...inputSearch }; | ||||
|         inputSearchNew[event.target.name] = event.target.value; | ||||
|         setInputSearch(inputSearchNew); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div className="container text-center"> | ||||
|             <div className="row justify-content-center"> | ||||
|                 {props.children} | ||||
|             </div> | ||||
|  | ||||
|             <div className="Functions__search-container row justify-content-center"> | ||||
|                 <select name="selectedCategory" value={inputSearch.selectedCategory} onChange={handleChange} className="Functions__select Functions__form-control"> | ||||
|                     <option value="0">Toutes catégories</option> | ||||
|                     {categories.map((category) => ( | ||||
|                         <option key={category.id} value={category.id} className="Functions__select-option" style={{ backgroundColor: category.color }}>{category.name}</option> | ||||
|                     ))} | ||||
|                 </select> | ||||
|                 <input value={inputSearch.search} onChange={handleChange} type="search" className="Functions__form-control Functions__search-input" name="search" id="search" placeholder="🔎 Rechercher..."></input> | ||||
|             </div> | ||||
|  | ||||
|             <div className="row justify-content-center"> | ||||
|                 {functionsData.rows.map((currentFunction, index) => { | ||||
|                     // Si c'est le dernier élément | ||||
|                     if (functionsData.rows.length === index + 1) { | ||||
|                         return <FunctionCard isAdmin={props.isAdmin} key={currentFunction.id} ref={lastFunctionCardRef} { ...currentFunction } />; | ||||
|                     } | ||||
|                     return <FunctionCard isAdmin={props.isAdmin} key={currentFunction.id} { ...currentFunction } />; | ||||
|                 })} | ||||
|             </div> | ||||
|             {isLoadingFunctions && <Loader />} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export default FunctionsList; | ||||
| @@ -37,7 +37,6 @@ export default function Header() { | ||||
|                     <ul className={`navbar__list ${(isActive) ? "navbar__list-active" : ""}`}> | ||||
|                         <NavigationLink name="Accueil" path="/" /> | ||||
|                         <NavigationLink name="Fonctions" path="/functions" /> | ||||
|  | ||||
|                         { | ||||
|                             (!isAuth) ?  | ||||
|                                 <Fragment> | ||||
| @@ -58,6 +57,10 @@ export default function Header() { | ||||
|                                     </li> | ||||
|                                 </Fragment> | ||||
|                         } | ||||
|                         { | ||||
|                             (isAuth && user.isAdmin) && | ||||
|                                 <NavigationLink name="Admin" path="/admin" /> | ||||
|                         } | ||||
|                     </ul> | ||||
|                 </nav> | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								website/pages/admin/[slug].js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								website/pages/admin/[slug].js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { Fragment } from 'react'; | ||||
| import Cookies from "universal-cookie"; | ||||
| import HeadTag from '../../components/HeadTag'; | ||||
| import redirect from '../../utils/redirect'; | ||||
|  | ||||
| const AdminFunctionComponent = (props) => { | ||||
|  | ||||
|     if (!props.user.isAdmin && typeof window != 'undefined') { | ||||
|         return redirect({}, '/404'); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <HeadTag /> | ||||
|             <p>{props.slug}</p> | ||||
|         </Fragment> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export async function getServerSideProps({ req, params }) { | ||||
|     const cookies = new Cookies(req.headers.cookie); | ||||
|     const { slug } = params; | ||||
|     return { | ||||
|         props: { | ||||
|             user: { ...cookies.get('user') }, | ||||
|             slug | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default AdminFunctionComponent; | ||||
							
								
								
									
										18
									
								
								website/pages/admin/addFunction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								website/pages/admin/addFunction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import Cookies from "universal-cookie"; | ||||
|  | ||||
| const addFunction = (props) => { | ||||
|     return ( | ||||
|         <p>Crée une nouvelle fonction</p> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export async function getServerSideProps({ req }) { | ||||
|     const cookies = new Cookies(req.headers.cookie); | ||||
|     return { | ||||
|         props: {  | ||||
|             user: { ...cookies.get('user') } | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default addFunction; | ||||
							
								
								
									
										39
									
								
								website/pages/admin/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								website/pages/admin/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { Fragment } from 'react'; | ||||
| import Link from 'next/link'; | ||||
| import Cookies from "universal-cookie"; | ||||
| import HeadTag from '../../components/HeadTag'; | ||||
| import FunctionsList from '../../components/FunctionsList/FunctionsList'; | ||||
| import redirect from '../../utils/redirect'; | ||||
|  | ||||
| const Admin = (props) => { | ||||
|  | ||||
|     if (!props.user.isAdmin && typeof window != 'undefined') { | ||||
|         return redirect({}, '/404'); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <HeadTag title="Admin - FunctionProject" description="Page d'administration de FunctionProject." /> | ||||
|  | ||||
|             <FunctionsList isAdmin> | ||||
|                 <div className="col-24"> | ||||
|                     <h1 className="Functions__title">Administration</h1> | ||||
|                     <Link href={"/admin/addFunction"}> | ||||
|                         <button style={{ margin: '0 0 40px 0' }} className="btn btn-dark">Crée une nouvelle fonction</button> | ||||
|                     </Link> | ||||
|                 </div> | ||||
|             </FunctionsList> | ||||
|         </Fragment> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export async function getServerSideProps({ req }) { | ||||
|     const cookies = new Cookies(req.headers.cookie); | ||||
|     return { | ||||
|         props: {  | ||||
|             user: { ...cookies.get('user') } | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default Admin; | ||||
| @@ -1,73 +1,9 @@ | ||||
| import { Fragment, useState, useEffect, useRef, useCallback } from 'react'; | ||||
| import { useRouter } from 'next/router'; | ||||
| import { Fragment } from 'react'; | ||||
| import HeadTag from '../../components/HeadTag'; | ||||
| import FunctionCard from '../../components/FunctionCard/FunctionCard'; | ||||
| import Loader from '../../components/Loader'; | ||||
| import api from '../../utils/api'; | ||||
| import useAPI from '../../hooks/useAPI'; | ||||
| import '../../public/css/pages/functions.css'; | ||||
| import FunctionsList from '../../components/FunctionsList/FunctionsList'; | ||||
|  | ||||
| const Functions = () => { | ||||
|  | ||||
|     const { categoryId } = useRouter().query; | ||||
|  | ||||
|     // State de recherche et de catégories | ||||
|     const [, categories]                = useAPI('/categories'); | ||||
|     const [inputSearch, setInputSearch] = useState({ search: "", selectedCategory: categoryId || "0" }); | ||||
|  | ||||
|     // State pour afficher les fonctions | ||||
|     const [functionsData, setFunctionsData]         = useState({ hasMore: true, rows: [] }); | ||||
|     const [isLoadingFunctions, setLoadingFunctions] = useState(true); | ||||
|     const [pageFunctions, setPageFunctions]         = useState(1); | ||||
|  | ||||
|     // Récupère la catégorie avec la query categoryId | ||||
|     useEffect(() => { | ||||
|         if (categoryId) { | ||||
|             handleChange({ target: { name: "selectedCategory", value: categoryId } }); | ||||
|         } | ||||
|     }, [categoryId]); | ||||
|  | ||||
|     // Récupère les fonctions si la page change | ||||
|     useEffect(() => { | ||||
|         getFunctionsData().then((data) => setFunctionsData({  | ||||
|             hasMore: data.hasMore,  | ||||
|             rows: [...functionsData.rows, ...data.rows]  | ||||
|         })); | ||||
|     }, [pageFunctions]); | ||||
|  | ||||
|     // Récupère les fonctions si la catégorie/recherche change | ||||
|     useEffect(() => { | ||||
|         getFunctionsData().then((data) => setFunctionsData(data)); | ||||
|     }, [inputSearch.selectedCategory, inputSearch.search]); | ||||
|  | ||||
|     // Permet la pagination au scroll | ||||
|     const observer = useRef(); | ||||
|     const lastFunctionCardRef = useCallback((node) => { | ||||
|         if (isLoadingFunctions) return; | ||||
|         if (observer.current) observer.current.disconnect(); | ||||
|         observer.current = new IntersectionObserver((entries) => { | ||||
|             if (entries[0].isIntersecting && functionsData.hasMore) { | ||||
|                 setPageFunctions(pageFunctions + 1); | ||||
|             } | ||||
|         }, { threshold: 1 }); | ||||
|         if (node) observer.current.observe(node); | ||||
|     }, [isLoadingFunctions, functionsData.hasMore]); | ||||
|  | ||||
|     const getFunctionsData = () => { | ||||
|         setLoadingFunctions(true); | ||||
|         return new Promise(async (next) => { | ||||
|             const result = await api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`); | ||||
|             setLoadingFunctions(false); | ||||
|             next(result.data); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     const handleChange = (event) => { | ||||
|         const inputSearchNew = { ...inputSearch }; | ||||
|         inputSearchNew[event.target.name] = event.target.value; | ||||
|         setInputSearch(inputSearchNew); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <Fragment> | ||||
|             <HeadTag  | ||||
| @@ -75,33 +11,10 @@ const Functions = () => { | ||||
|                 description="Liste des fonctions."  | ||||
|                 image="/images/FunctionProject_icon_small.png"  | ||||
|             /> | ||||
|              | ||||
|             <div className="container text-center"> | ||||
|                 <div className="row justify-content-center"> | ||||
|                     <h1 className="Functions__title">Fonctions</h1> | ||||
|                 </div> | ||||
|  | ||||
|                 <div className="Functions__search-container row justify-content-center"> | ||||
|                     <select name="selectedCategory" value={inputSearch.selectedCategory} onChange={handleChange} className="Functions__select Functions__form-control"> | ||||
|                         <option value="0">Toutes catégories</option> | ||||
|                         {categories.map((category) => ( | ||||
|                             <option key={category.id} value={category.id} className="Functions__select-option" style={{ backgroundColor: category.color }}>{category.name}</option> | ||||
|                         ))} | ||||
|                     </select> | ||||
|                     <input value={inputSearch.search} onChange={handleChange} type="search" className="Functions__form-control Functions__search-input" name="search" id="search" placeholder="🔎 Rechercher..."></input> | ||||
|                 </div> | ||||
|      | ||||
|                 <div className="row justify-content-center"> | ||||
|                     {functionsData.rows.map((f, index) => { | ||||
|                         // Si c'est le dernier élément | ||||
|                         if (functionsData.rows.length === index + 1) { | ||||
|                             return <FunctionCard ref={lastFunctionCardRef} key={f.id} slug={f.slug} image={f.image} title={f.title} description={f.description} category={f.categorie} publicationDate={new Date(f.createdAt).toLocaleDateString('fr-FR')} type={f.type} />; | ||||
|                         } | ||||
|                         return <FunctionCard key={f.id} slug={f.slug} image={f.image} title={f.title} description={f.description} category={f.categorie} publicationDate={new Date(f.createdAt).toLocaleDateString('fr-FR')} type={f.type} />; | ||||
|                     })} | ||||
|                 </div> | ||||
|                 {isLoadingFunctions && <Loader />} | ||||
|             </div> | ||||
|             <FunctionsList> | ||||
|                 <h1 className="Functions__title">Fonctions</h1> | ||||
|             </FunctionsList> | ||||
|         </Fragment> | ||||
|     ); | ||||
| } | ||||
|   | ||||
| @@ -169,7 +169,7 @@ const Profile = (props) => { | ||||
|                             <div className="row justify-content-center"> | ||||
|                                 {props.favoritesArray.map((favorite) => { | ||||
|                                     return ( | ||||
|                                         <FunctionCard key={favorite.id} slug={favorite.slug} image={favorite.image} title={favorite.title} description={favorite.description} type={favorite.type} category={favorite.categorie} publicationDate={date.format(new Date(favorite.createdAt), 'DD/MM/YYYY', true)} /> | ||||
|                                         <FunctionCard key={favorite.id} { ...favorite } /> | ||||
|                                     ); | ||||
|                                 })} | ||||
|                             </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user