frontend: FunctionsList et début de /admin
This commit is contained in:
parent
02058fc2fb
commit
c157f7e922
@ -1,5 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState, forwardRef } from 'react';
|
import { useState, forwardRef } from 'react';
|
||||||
|
import date from 'date-and-time';
|
||||||
import Loader from '../Loader';
|
import Loader from '../Loader';
|
||||||
import './FunctionCard.css';
|
import './FunctionCard.css';
|
||||||
import { API_URL } from '../../utils/config';
|
import { API_URL } from '../../utils/config';
|
||||||
@ -16,8 +17,18 @@ const FunctionCard = forwardRef((props, ref) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<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 */}
|
{/* 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"}>
|
<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>
|
<p className="FunctionCard__description">{props.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="FunctionCard__info">
|
<div className="FunctionCard__info">
|
||||||
<p className="FunctionCard__category" style={{ backgroundColor: props.category.color }}>{props.category.name}</p>
|
<p className="FunctionCard__category" style={{ backgroundColor: props.categorie.color }}>{props.categorie.name}</p>
|
||||||
<p className="FunctionCard__publication-date">{props.publicationDate}</p>
|
<p className="FunctionCard__publication-date">{date.format(new Date(props.createdAt), 'DD/MM/YYYY', true)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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" : ""}`}>
|
<ul className={`navbar__list ${(isActive) ? "navbar__list-active" : ""}`}>
|
||||||
<NavigationLink name="Accueil" path="/" />
|
<NavigationLink name="Accueil" path="/" />
|
||||||
<NavigationLink name="Fonctions" path="/functions" />
|
<NavigationLink name="Fonctions" path="/functions" />
|
||||||
|
|
||||||
{
|
{
|
||||||
(!isAuth) ?
|
(!isAuth) ?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@ -58,6 +57,10 @@ export default function Header() {
|
|||||||
</li>
|
</li>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
(isAuth && user.isAdmin) &&
|
||||||
|
<NavigationLink name="Admin" path="/admin" />
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</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 { Fragment } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import HeadTag from '../../components/HeadTag';
|
import HeadTag from '../../components/HeadTag';
|
||||||
import FunctionCard from '../../components/FunctionCard/FunctionCard';
|
import FunctionsList from '../../components/FunctionsList/FunctionsList';
|
||||||
import Loader from '../../components/Loader';
|
|
||||||
import api from '../../utils/api';
|
|
||||||
import useAPI from '../../hooks/useAPI';
|
|
||||||
import '../../public/css/pages/functions.css';
|
|
||||||
|
|
||||||
const Functions = () => {
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<HeadTag
|
<HeadTag
|
||||||
@ -76,32 +12,9 @@ const Functions = () => {
|
|||||||
image="/images/FunctionProject_icon_small.png"
|
image="/images/FunctionProject_icon_small.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container text-center">
|
<FunctionsList>
|
||||||
<div className="row justify-content-center">
|
|
||||||
<h1 className="Functions__title">Fonctions</h1>
|
<h1 className="Functions__title">Fonctions</h1>
|
||||||
</div>
|
</FunctionsList>
|
||||||
|
|
||||||
<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>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ const Profile = (props) => {
|
|||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
{props.favoritesArray.map((favorite) => {
|
{props.favoritesArray.map((favorite) => {
|
||||||
return (
|
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>
|
</div>
|
||||||
|
Reference in New Issue
Block a user