👌 IMPROVE: Refactoring /functions/[slug]
This commit is contained in:
33
website/components/FunctionPage/CommentCard/CommentCard.css
Normal file
33
website/components/FunctionPage/CommentCard/CommentCard.css
Normal file
@ -0,0 +1,33 @@
|
||||
.CommentCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-wrap: break-word;
|
||||
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25);
|
||||
border: 1px solid black;
|
||||
border-radius: .7em;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
.CommentCard__container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
.CommentCard__user-logo {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CommentCard__message-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.CommentCard__message {
|
||||
line-height: 1.8;
|
||||
margin: 15px 0 0 0;
|
||||
}
|
52
website/components/FunctionPage/CommentCard/CommentCard.js
Normal file
52
website/components/FunctionPage/CommentCard/CommentCard.js
Normal file
@ -0,0 +1,52 @@
|
||||
import Link from 'next/link';
|
||||
import { forwardRef, useContext, Fragment } from 'react';
|
||||
import date from 'date-and-time';
|
||||
import { UserContext } from '../../../contexts/UserContext';
|
||||
import { API_URL } from '../../../utils/config';
|
||||
import api from '../../../utils/api';
|
||||
import './CommentCard.css';
|
||||
|
||||
const CommentCard = forwardRef((props, ref) => {
|
||||
|
||||
const { isAuth, user } = useContext(UserContext);
|
||||
|
||||
const deleteCommentById = async () => {
|
||||
props.manageComment.setLoadingComments(true);
|
||||
if (isAuth && user.token != undefined) {
|
||||
try {
|
||||
await api.delete(`/comments?commentId=${props.id}`, { headers: { 'Authorization': user.token } });
|
||||
const newCommentsData = { ...props.manageComment.commentsData };
|
||||
const commentIndex = newCommentsData.rows.findIndex((value) => value.id === props.id);
|
||||
newCommentsData.rows.splice(commentIndex, 1);
|
||||
props.manageComment.setCommentsData({ hasMore: props.manageComment.commentsData.hasMore, rows: newCommentsData.rows });
|
||||
} catch {}
|
||||
}
|
||||
props.manageComment.setLoadingComments(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className="CommentCard col-24">
|
||||
<div className="CommentCard__container">
|
||||
<div className="row">
|
||||
<Link href={"/profile/[name]"} as={`/profile/${props.user.name}`}>
|
||||
<img className="CommentCard__user-logo" src={API_URL + props.user.logo} alt={props.user.name} />
|
||||
</Link>
|
||||
<span className="CommentCard__message-info">
|
||||
<Link href={"/profile/[name]"} as={`/profile/${props.user.name}`}>
|
||||
<a>{props.user.name}</a>
|
||||
</Link>
|
||||
- {date.format(new Date(props.createdAt), 'DD/MM/YYYY à HH:mm', true)}
|
||||
{(isAuth && user.name === props.user.name) && <Fragment> - <a onClick={deleteCommentById} href="#">supprimer</a></Fragment>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="row">
|
||||
<p className="CommentCard__message">
|
||||
{props.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default CommentCard;
|
11
website/components/FunctionPage/FunctionArticle.js
Normal file
11
website/components/FunctionPage/FunctionArticle.js
Normal file
@ -0,0 +1,11 @@
|
||||
import htmlParser from 'html-react-parser';
|
||||
|
||||
const FunctionArticle = ({ article }) => {
|
||||
return (
|
||||
<div style={{ marginBottom: '50px' }} className="container-fluid">
|
||||
{(article != undefined) ? htmlParser(article) : <p className="text-center">L'article n'est pas encore disponible.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionArticle;
|
@ -0,0 +1,7 @@
|
||||
.FunctionComments__row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.FunctionComments__textarea {
|
||||
height: auto;
|
||||
resize: vertical;
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import { Fragment, useState, useEffect, useContext, useRef, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { UserContext } from '../../../contexts/UserContext';
|
||||
import CommentCard from '../CommentCard/CommentCard';
|
||||
import Loader from '../../Loader';
|
||||
import api from '../../../utils/api';
|
||||
import './FunctionComments.css';
|
||||
|
||||
const FunctionComments = ({ functionId }) => {
|
||||
|
||||
// State pour poster un commentaire
|
||||
const { isAuth, user } = useContext(UserContext);
|
||||
const [inputState, setInputState] = useState({});
|
||||
|
||||
// State pour afficher les commentaires
|
||||
const [commentsData, setCommentsData] = useState({ hasMore: true, rows: [] });
|
||||
const [isLoadingComments, setLoadingComments] = useState(true);
|
||||
const [pageComments, setPageComments] = useState(1);
|
||||
|
||||
// Récupère les commentaires si la page change
|
||||
useEffect(() => {
|
||||
getCommentsData().then((data) => setCommentsData({
|
||||
hasMore: data.hasMore,
|
||||
rows: [...commentsData.rows, ...data.rows]
|
||||
}));
|
||||
}, [pageComments]);
|
||||
|
||||
// Permet la pagination au scroll
|
||||
const observer = useRef();
|
||||
const lastCommentCardRef = useCallback((node) => {
|
||||
if (isLoadingComments) return;
|
||||
if (observer.current) observer.current.disconnect();
|
||||
observer.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && commentsData.hasMore) {
|
||||
setPageComments(pageComments + 1);
|
||||
}
|
||||
}, { threshold: 1 });
|
||||
if (node) observer.current.observe(node);
|
||||
}, [isLoadingComments, commentsData.hasMore]);
|
||||
|
||||
const getCommentsData = () => {
|
||||
setLoadingComments(true);
|
||||
return new Promise(async (next) => {
|
||||
const result = await api.get(`/comments/${functionId}/?page=${pageComments}&limit=10`);
|
||||
setLoadingComments(false);
|
||||
next(result.data);
|
||||
});
|
||||
}
|
||||
|
||||
const handleChange = (event) => {
|
||||
const inputStateNew = { ...inputState };
|
||||
inputStateNew[event.target.name] = event.target.value;
|
||||
setInputState(inputStateNew);
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
setLoadingComments(true);
|
||||
event.preventDefault();
|
||||
const token = user.token;
|
||||
if (isAuth && token != undefined && inputState.commentPost != undefined) {
|
||||
try {
|
||||
const response = await api.post(`/comments/${functionId}`, { message: inputState.commentPost }, { headers: { 'Authorization': token } });
|
||||
const comment = { ...response.data, user: { name: user.name, logo: user.logo } };
|
||||
setCommentsData({ hasMore: commentsData.hasMore, rows: [comment, ...commentsData.rows] });
|
||||
setInputState({ commentPost: "" });
|
||||
} catch { }
|
||||
}
|
||||
setLoadingComments(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="FunctionComments__post container-fluid">
|
||||
<div className="row FunctionComments__row">
|
||||
<div className="col-24">
|
||||
{
|
||||
(isAuth) ?
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group FunctionComments__post-group">
|
||||
<label className="form-label" htmlFor="commentPost">Ajouter un commentaire :</label>
|
||||
<textarea className="FunctionComments__textarea form-control" value={inputState.commentPost} onChange={handleChange} name="commentPost" id="commentPost" placeholder="Idée d'amélioration, avis, remarque, partage d'expérience personnel, ..."></textarea>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginTop: '0.7em' }}>
|
||||
<button type="submit" className="btn btn-dark">Envoyer</button>
|
||||
</div>
|
||||
</form>
|
||||
:
|
||||
<p className="text-center">
|
||||
Vous devez être <Link href={'/login'}><a>connecté</a></Link> pour poster un commentaire.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-fluid">
|
||||
{isLoadingComments &&
|
||||
<div className="row justify-content-center">
|
||||
<Loader />
|
||||
</div>
|
||||
}
|
||||
<div className="row justify-content-center">
|
||||
{commentsData.rows.map((comment, index) => {
|
||||
// Si c'est le dernier élément
|
||||
if (commentsData.rows.length === index + 1) {
|
||||
return <CommentCard key={comment.id} ref={lastCommentCardRef} { ...comment } manageComment={{ setCommentsData, commentsData, setLoadingComments }} />;
|
||||
}
|
||||
return <CommentCard key={comment.id} { ...comment } manageComment={{ setCommentsData, commentsData, setLoadingComments }} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionComments;
|
67
website/components/FunctionPage/FunctionComponentTop.js
Normal file
67
website/components/FunctionPage/FunctionComponentTop.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faStar } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStar as farStar } from '@fortawesome/free-regular-svg-icons';
|
||||
import date from 'date-and-time';
|
||||
import { UserContext } from '../../contexts/UserContext';
|
||||
import api from '../../utils/api';
|
||||
import { API_URL } from '../../utils/config';
|
||||
import '../FunctionCard/FunctionCard.css';
|
||||
|
||||
const FunctionComponentTop = (props) => {
|
||||
|
||||
const { isAuth, user } = useContext(UserContext);
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuth && user.token != undefined) {
|
||||
fetchFavorite();
|
||||
}
|
||||
}, [isAuth]);
|
||||
|
||||
const fetchFavorite = async () => {
|
||||
try {
|
||||
const favoriteResponse = await api.get(`/favorites/${props.id}`, { headers: { 'Authorization': user.token } });
|
||||
setIsFavorite(favoriteResponse.data.isFavorite);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const toggleFavorite = async () => {
|
||||
if (isAuth && user.token != undefined) {
|
||||
try {
|
||||
if (isFavorite) {
|
||||
const response = await api.delete(`/favorites/${props.id}`, { headers: { 'Authorization': user.token } });
|
||||
if (response.status === 200) return setIsFavorite(false);
|
||||
}
|
||||
const response = await api.post(`/favorites/${props.id}`, {}, { headers: { 'Authorization': user.token } });
|
||||
if (response.status === 201) return setIsFavorite(true);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row justify-content-center text-center">
|
||||
<div className="FunctionComponent__top col-24">
|
||||
|
||||
{(isAuth) &&
|
||||
<FontAwesomeIcon onClick={toggleFavorite} { ...(isFavorite) ? { icon: faStar } : { icon: farStar } } title={(isFavorite) ? "Retirer la fonction des favoris" : "Ajouter la fonction aux favoris"} className="FunctionComponent__star-favorite" />
|
||||
}
|
||||
|
||||
<img className="FunctionComponent__image" src={API_URL + props.image} alt={props.title} />
|
||||
<h1 className="FunctionComponent__title title-important">{props.title}</h1>
|
||||
<p className="FunctionComponent__description">{props.description}</p>
|
||||
<div className="FunctionCard__info">
|
||||
<Link href={`/functions?categoryId=${props.categorieId}`}>
|
||||
<a className="FunctionCard__category" style={{ backgroundColor: props.categorie.color, color: 'inherit' }}>{props.categorie.name}</a>
|
||||
</Link>
|
||||
<p className="FunctionCard__publication-date">{date.format(new Date(props.createdAt), 'DD/MM/YYYY', true)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionComponentTop;
|
161
website/components/FunctionPage/FunctionForm.js
Normal file
161
website/components/FunctionPage/FunctionForm.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { Fragment, useState, useEffect } from 'react';
|
||||
import Loader from '../Loader';
|
||||
import htmlParser from 'html-react-parser';
|
||||
import dynamic from 'next/dynamic';
|
||||
import api from '../../utils/api';
|
||||
import fr from 'date-fns/locale/fr';
|
||||
import { registerLocale } from "react-datepicker";
|
||||
import date from 'date-and-time';
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
registerLocale('fr', fr);
|
||||
|
||||
const FunctionForm = (props) => {
|
||||
|
||||
const [inputState, setInputState] = useState({});
|
||||
const [message, setMessage] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// inputState par défaut
|
||||
useEffect(() => {
|
||||
const inputStateNew = { ...inputState };
|
||||
props.inputsArray.forEach((input) => {
|
||||
if (input.type === "select" && input.options.length > 0) {
|
||||
inputStateNew[input.name] = input.options[0].value;
|
||||
}
|
||||
});
|
||||
setInputState(inputStateNew);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
setIsLoading(true);
|
||||
event.preventDefault();
|
||||
api.post(`/functions/${props.slug}`, inputState)
|
||||
.then((response) => {
|
||||
setMessage(response.data.resultHTML);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setMessage(error.response.data.message);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
const handleChange = (event) => {
|
||||
const inputStateNew = { ...inputState };
|
||||
inputStateNew[event.target.name] = event.target.value;
|
||||
setInputState(inputStateNew);
|
||||
}
|
||||
|
||||
if (props.inputsArray.length <= 0) {
|
||||
return (
|
||||
<div className="FunctionComponent__slide text-center">
|
||||
<p>La fonction n'est pas encore disponible.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{props.inputsArray.map((input, index) => {
|
||||
switch (input.type) {
|
||||
case "text":
|
||||
return (
|
||||
<div key={index} className="form-group">
|
||||
<label className="form-label" htmlFor={input.name}>{input.label}</label>
|
||||
<input onChange={handleChange} type="text" name={input.name} id={input.name} placeholder={input.placeholder} className="form-control" />
|
||||
</div>
|
||||
);
|
||||
case "integer":
|
||||
case "float":
|
||||
return (
|
||||
<div key={index} className="form-group">
|
||||
<label className="form-label" htmlFor={input.name}>{input.label}</label>
|
||||
<input onChange={handleChange} type="number" step={(input.type === "integer") ? "1" : "0.01"} name={input.name} id={input.name} placeholder={input.placeholder} className="form-control" />
|
||||
</div>
|
||||
);
|
||||
case "calendar":
|
||||
// Permet au datepicker de prendre la hauteur
|
||||
if (typeof window != 'undefined') {
|
||||
const newScript = document.createElement("script");
|
||||
newScript.src = "/js/extraHeightCSS.js";
|
||||
document.body.appendChild(newScript);
|
||||
}
|
||||
const DatePicker = dynamic(
|
||||
() => import('react-datepicker'),
|
||||
{ ssr: false }
|
||||
);
|
||||
return (
|
||||
<div key={index} className="form-group">
|
||||
<label className="form-label" htmlFor={input.name}>{input.label}</label>
|
||||
<br/>
|
||||
<DatePicker
|
||||
selected={(() => {
|
||||
try {
|
||||
if (inputState[input.name] != undefined) {
|
||||
const dateArray = inputState[input.name].split('/');
|
||||
const year = dateArray[2];
|
||||
const month = dateArray[1];
|
||||
const day = dateArray[0];
|
||||
return new Date(year, parseInt(month) - 1, parseInt(day) + 1);
|
||||
}
|
||||
throw "Not a valid date";
|
||||
} catch {
|
||||
return new Date();
|
||||
}
|
||||
})()}
|
||||
locale="fr"
|
||||
dateFormat="dd/MM/yyyy"
|
||||
fixedHeight
|
||||
placeholderText={input.placeholder}
|
||||
onChange={(dateObject) => {
|
||||
try {
|
||||
const formattedDate = date.format(dateObject, 'DD/MM/YYYY', true);
|
||||
handleChange({
|
||||
target: {
|
||||
name: input.name,
|
||||
value: formattedDate
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "select":
|
||||
return (
|
||||
<div key={index} className="form-group">
|
||||
<label className="form-label" htmlFor={input.name}>{input.label}</label>
|
||||
<select onChange={handleChange} name={input.name} id={input.name} value={inputState[input.name] || input.options[0]} className="form-control">
|
||||
{input.options.map((option, optionIndex) => {
|
||||
return (
|
||||
<option key={optionIndex} value={option.value}>{option.name}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Erreur, l'input n'est pas valide...</p>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
<div className="form-group text-center">
|
||||
<button type="submit" className="btn btn-dark">Envoyer</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="form-result text-center">
|
||||
{
|
||||
(isLoading) ?
|
||||
<Loader />
|
||||
:
|
||||
htmlParser(message)
|
||||
}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default FunctionForm;
|
@ -2,7 +2,7 @@ import { Fragment, useState } from 'react';
|
||||
import { API_URL } from '../../utils/config';
|
||||
import HeadTag from '../HeadTag';
|
||||
import FunctionTabsTop from './FunctionTabsTop';
|
||||
import FunctionComponentTop from '../FunctionComponentTop';
|
||||
import FunctionComponentTop from './FunctionComponentTop';
|
||||
|
||||
const FunctionPage = (props) => {
|
||||
|
||||
|
41
website/components/FunctionPage/FunctionTabs.css
Normal file
41
website/components/FunctionPage/FunctionTabs.css
Normal file
@ -0,0 +1,41 @@
|
||||
.FunctionTabs__nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid #d9e2ef;
|
||||
margin-bottom: -1px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.FunctionTabs__nav-item {
|
||||
margin-bottom: -1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.FunctionTabs__nav-link {
|
||||
color: var(--text-color);
|
||||
border: 1px solid #0c0b0b38;
|
||||
border-bottom: 0px;
|
||||
border-top-left-radius: .375rem;
|
||||
border-top-right-radius: .375rem;
|
||||
display: block;
|
||||
padding: .5rem 1rem;
|
||||
transition: .2s;
|
||||
}
|
||||
.FunctionTabs__nav-link-active {
|
||||
border-color: #d9e2ef #d9e2ef #fff;
|
||||
color: var(--important);
|
||||
}
|
||||
.FunctionTabs__nav-link:hover {
|
||||
border-color: #f1f4f8 #f1f4f8 #d9e2ef;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 490px) {
|
||||
.FunctionTabs__nav {
|
||||
flex-direction: column;
|
||||
}
|
||||
.FunctionTabs__nav-link {
|
||||
border-color: #f1f4f8 #f1f4f8 #d9e2ef;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import '../../components/FunctionTabs/FunctionTabs.css';
|
||||
import './FunctionTabs.css';
|
||||
|
||||
const FunctionTabs = (props) => {
|
||||
return (
|
||||
|
Reference in New Issue
Block a user