frontend: Loader + Refactoring

This commit is contained in:
Divlo 2020-03-21 16:43:37 +01:00
parent a0fb5ee13a
commit 895d0c7f6b
8 changed files with 144 additions and 64 deletions

View File

@ -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);
} }

View File

@ -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;

View 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);
}
}

View 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
View 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;

View File

@ -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>

View File

@ -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;

View File

@ -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);