frontend: Loader + Refactoring
This commit is contained in:
parent
a0fb5ee13a
commit
895d0c7f6b
@ -11,6 +11,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all .3s;
|
transition: all .3s;
|
||||||
}
|
}
|
||||||
|
.FunctionCard__container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
.FunctionCard:hover {
|
.FunctionCard:hover {
|
||||||
transform: translateY(-7px);
|
transform: translateY(-7px);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,24 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useState, Fragment } from 'react';
|
||||||
|
import Loader from '../Loader/Loader';
|
||||||
import './FunctionCard.css';
|
import './FunctionCard.css';
|
||||||
|
|
||||||
const FunctionCard = (props) => (
|
const FunctionCard = (props) => {
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleLoad = () => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Link href={`/functions/${props.slug}`}>
|
<Link href={`/functions/${props.slug}`}>
|
||||||
<div className="FunctionCard col-sm-24 col-md-10 col-xl-7">
|
<Fragment>
|
||||||
|
<div className={"FunctionCard col-sm-24 col-md-10 col-xl-7"}>
|
||||||
|
{isLoading && <Loader width="100px" height="100px" />}
|
||||||
|
<div className={`FunctionCard__container ${isLoading ? "d-none" : ""}`}>
|
||||||
<div className="FunctionCard__top">
|
<div className="FunctionCard__top">
|
||||||
<img className="FunctionCard__image" alt={props.title} src={props.image} />
|
<img onLoad={handleLoad} className="FunctionCard__image" alt={props.title} src={props.image} />
|
||||||
<h2 className="FunctionCard__title">{props.title}</h2>
|
<h2 className="FunctionCard__title">{props.title}</h2>
|
||||||
<p className="FunctionCard__description">{props.description}</p>
|
<p className="FunctionCard__description">{props.description}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -14,7 +27,10 @@ const FunctionCard = (props) => (
|
|||||||
<p className="FunctionCard__publication-date">{props.publicationDate}</p>
|
<p className="FunctionCard__publication-date">{props.publicationDate}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default FunctionCard;
|
export default FunctionCard;
|
13
frontend/components/Loader/Loader.css
Normal file
13
frontend/components/Loader/Loader.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.Loader {
|
||||||
|
transform-origin: 50% 50%; animation: .9s linear 0s infinite normal forwards running Loader__spin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes Loader__spin {
|
||||||
|
0% {
|
||||||
|
animation-timing-function: cubic-bezier(0.5856,0.0703,0.4143,0.9297);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
11
frontend/components/Loader/Loader.js
Normal file
11
frontend/components/Loader/Loader.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import './Loader.css';
|
||||||
|
|
||||||
|
const Loader = ({ width, height }) => (
|
||||||
|
<svg width={width} height={height} viewBox="0 0 100 100">
|
||||||
|
<g transform="translate(50 50) rotate(0) scale(1 1) translate(-50 -50)">
|
||||||
|
<image className="Loader" x="0" y="0" width="100" height="100" href="/images/FunctionProject_icon.png"></image>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Loader;
|
31
frontend/hooks/useAPI.js
Normal file
31
frontend/hooks/useAPI.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import api from '../config/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} url
|
||||||
|
* @param {*} defaultData
|
||||||
|
* @param {String} method
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
function useAPI(url, defaultData = [], method = "get", options = {}) {
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [data, setData] = useState(defaultData);
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api[method](url, options)
|
||||||
|
.then((result) => {
|
||||||
|
setData(result.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setHasError(true);
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [isLoading, data, hasError];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAPI;
|
@ -1,60 +1,50 @@
|
|||||||
import { Fragment, useState, useEffect } from 'react';
|
import { Fragment, useState, useEffect } from 'react';
|
||||||
import HeadTag from '../components/HeadTag';
|
import HeadTag from '../components/HeadTag';
|
||||||
import FunctionCard from '../components/FunctionCard/FunctionCard';
|
import FunctionCard from '../components/FunctionCard/FunctionCard';
|
||||||
|
import Loader from '../components/Loader/Loader';
|
||||||
import '../public/css/pages/functions.css';
|
import '../public/css/pages/functions.css';
|
||||||
import { API_URL } from '../config/config';
|
import { API_URL } from '../config/config';
|
||||||
import api from '../config/api';
|
import api from '../config/api';
|
||||||
|
import useAPI from '../hooks/useAPI';
|
||||||
|
|
||||||
const Functions = () => {
|
const Functions = () => {
|
||||||
|
|
||||||
// State de recherche et de catégories
|
// State de recherche et de catégories
|
||||||
const [categories, setCategories] = useState([]);
|
const [, categories] = useAPI('/categories');
|
||||||
const [inputSearch, setInputSearch] = useState({ search: "", selectedCategory: "0" });
|
const [inputSearch, setInputSearch] = useState({ search: "", selectedCategory: "0" });
|
||||||
|
|
||||||
// State pour afficher les fonctions
|
// State pour afficher les fonctions
|
||||||
const [functions, setFunctions] = useState([]);
|
const [functionsData, setFunctionsData] = useState({ hasMore: true, rows: [] });
|
||||||
const [isLoadingFunctions, setLoadingFunctions] = useState(true);
|
const [isLoadingFunctions, setLoadingFunctions] = useState(true);
|
||||||
const [pageFunctions, setPageFunctions] = useState(1);
|
const [pageFunctions, setPageFunctions] = useState(1);
|
||||||
const [hasMoreFunctions, sethasMoreFunctions] = useState(false);
|
|
||||||
|
|
||||||
// Récupère les catégories
|
const getFunctionsData = () => {
|
||||||
useEffect(() => {
|
setLoadingFunctions(true);
|
||||||
api.get('/categories')
|
return new Promise(async (next) => {
|
||||||
.then((result) => {
|
const result = await api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`);
|
||||||
setCategories(result.data);
|
setLoadingFunctions(false);
|
||||||
})
|
next(result.data);
|
||||||
.catch((error) => console.error(error));
|
});
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
// Récupère les fonctions si la page change
|
// Récupère les fonctions si la page change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`)
|
getFunctionsData().then((data) => setFunctionsData({
|
||||||
.then((result) => {
|
hasMore: data.hasMore,
|
||||||
setLoadingFunctions(false);
|
rows: [...functionsData.rows, ...data.rows]
|
||||||
sethasMoreFunctions(result.data.hasMore);
|
}));
|
||||||
setFunctions([...functions, ...result.data.rows]);
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}, [pageFunctions]);
|
}, [pageFunctions]);
|
||||||
|
|
||||||
// Récupère les fonctions si la catégorie/recherche change
|
// Récupère les fonctions si la catégorie/recherche change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get(`/functions?page=${pageFunctions}&limit=10&categoryId=${inputSearch.selectedCategory}&search=${inputSearch.search}`)
|
getFunctionsData().then((data) => setFunctionsData(data));
|
||||||
.then((result) => {
|
|
||||||
setLoadingFunctions(false);
|
|
||||||
sethasMoreFunctions(result.data.hasMore);
|
|
||||||
setFunctions(result.data.rows);
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}, [inputSearch.selectedCategory, inputSearch.search]);
|
}, [inputSearch.selectedCategory, inputSearch.search]);
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
setLoadingFunctions(true);
|
|
||||||
setPageFunctions(pageFunctions + 1);
|
setPageFunctions(pageFunctions + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (event) => {
|
const handleChange = (event) => {
|
||||||
setLoadingFunctions(true);
|
|
||||||
const inputSearchNew = { ...inputSearch };
|
const inputSearchNew = { ...inputSearch };
|
||||||
inputSearchNew[event.target.name] = event.target.value;
|
inputSearchNew[event.target.name] = event.target.value;
|
||||||
setInputSearch(inputSearchNew);
|
setInputSearch(inputSearchNew);
|
||||||
@ -84,18 +74,19 @@ const Functions = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
{functions.map((f) => (
|
{functionsData.rows.map((f) => (
|
||||||
<FunctionCard key={f.id} slug={f.slug} image={API_URL + f.image} title={f.title} description={f.description} category={f.categorie} publicationDate={new Date(f.createdAt).toLocaleDateString('fr-FR')} />
|
<FunctionCard key={f.id} slug={f.slug} image={API_URL + f.image} title={f.title} description={f.description} category={f.categorie} publicationDate={new Date(f.createdAt).toLocaleDateString('fr-FR')} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
!isLoadingFunctions && hasMoreFunctions
|
isLoadingFunctions ?
|
||||||
?
|
<Loader width="100px" height="100px" />
|
||||||
<button className="btn btn-dark" onClick={loadMore}>Charger plus de fonctions ?</button>
|
: functionsData.hasMore ?
|
||||||
: !hasMoreFunctions ?
|
<div className="row justify-content-center">
|
||||||
null
|
<button className="btn btn-dark" style={{marginBottom: "50px"}} onClick={loadMore}>Charger plus de fonctions ?</button>
|
||||||
|
</div>
|
||||||
:
|
:
|
||||||
<p>Chargement...</p>
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment, useEffect } from 'react';
|
||||||
import HeadTag from '../components/HeadTag';
|
import HeadTag from '../components/HeadTag';
|
||||||
|
|
||||||
const Home = () => (
|
const Home = () => {
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
'%c ⚙️ FunctionProject',
|
||||||
|
'color: #ffd800; font-weight: bold; background-color: #181818;padding: 10px;border-radius: 10px;font-size: 20px'
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<HeadTag
|
<HeadTag
|
||||||
title="FunctionProject"
|
title="FunctionProject"
|
||||||
@ -9,8 +18,8 @@ const Home = () => (
|
|||||||
image="/images/FunctionProject_icon_small.png"
|
image="/images/FunctionProject_icon_small.png"
|
||||||
/>
|
/>
|
||||||
<div>Home</div>
|
<div>Home</div>
|
||||||
{console.log('%c ⚙️ FunctionProject', 'color: #ffd800; font-weight: bold; background-color: #181818;padding: 10px;border-radius: 10px;font-size: 20px')}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
@ -60,6 +60,9 @@ a, .important {
|
|||||||
color: var(--important);
|
color: var(--important);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.d-none {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
.form-control {
|
.form-control {
|
||||||
display: block;
|
display: block;
|
||||||
height: calc(1.5em + .75rem + 2px);
|
height: calc(1.5em + .75rem + 2px);
|
||||||
|
Reference in New Issue
Block a user