📦 NEW: frontend: Modifier info et Article

This commit is contained in:
Divlo 2020-04-15 22:50:40 +02:00
parent 5eb64d200b
commit 9c5d1fc06b
12 changed files with 359 additions and 182 deletions

View File

@ -0,0 +1,129 @@
import { Fragment, useState, useEffect } from 'react';
import htmlParser from 'html-react-parser';
import Loader from '../components/Loader';
import useAPI from '../hooks/useAPI';
import api from '../utils/api';
import '../public/css/pages/admin.css';
const AddEditFunction = (props) => {
const [, categories] = useAPI('/categories');
const [inputState, setInputState] = useState(props.defaultInputState);
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (categories.length > 0 && !props.isEditing) {
handleChange({
target: {
name: "categorieId",
value: categories[0].id
}
});
}
}, [categories]);
const apiCallFunction = (formData) => {
if (props.isEditing) return api.put(`/admin/functions/${inputState.id}`, formData, { headers: { 'Authorization': props.user.token } });
return api.post('/admin/functions', formData, { headers: { 'Authorization': props.user.token } });
}
const handleChange = (event, isTypeCheck = false) => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = (event.target.files != undefined) ? event.target.files[0] : (isTypeCheck) ? event.target.checked : event.target.value;
setInputState(inputStateNew);
}
const handleSubmit = (event) => {
event.preventDefault();
setIsLoading(true);
const formData = new FormData();
formData.append('type', inputState.type);
formData.append('categorieId', inputState.categorieId);
formData.append('title', inputState.title);
formData.append('slug', inputState.slug);
formData.append('description', inputState.description);
formData.append('image', inputState.image);
if (props.isEditing) {
formData.append('isOnline', inputState.isOnline);
}
apiCallFunction(formData)
.then(() => {
setIsLoading(false);
window.location.reload(true);
})
.catch((error) => {
setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`);
setIsLoading(false);
});
}
return (
<Fragment>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label" htmlFor="title">Titre :</label>
<input value={inputState.title} onChange={handleChange} type="text" name="title" id="title" className="form-control" placeholder="(e.g : Nombre aléatoire)" />
</div>
<div className="form-group">
<label className="form-label" htmlFor="slug">Slug :</label>
<input value={inputState.slug} onChange={handleChange} type="text" name="slug" id="slug" className="form-control" placeholder="(e.g : randomNumber)" />
</div>
<div className="form-group">
<label className="form-label" htmlFor="description">Description :</label>
<textarea style={{ height: 'auto' }} value={inputState.description} onChange={handleChange} name="description" id="description" className="form-control" rows="5"></textarea>
</div>
<div className="form-group">
<label className="form-label" htmlFor="type">Type :</label>
<select onChange={handleChange} name="type" id="type" className="form-control" { ...(props.isEditing) && { value: inputState.type } }>
<option value="form">Formulaire</option>
<option value="article">Article</option>
<option value="page">Page</option>
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="categorieId">Catégorie :</label>
<select onChange={handleChange} name="categorieId" id="categorieId" className="form-control" { ...(props.isEditing) && { value: inputState.categorieId } }>
{categories.map((category) => (
<option key={category.id} value={category.id} className="Admin__Modal-select-option" style={{ backgroundColor: category.color }}>{category.name}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="image">Image <em>(150x150 recommandé)</em> :</label>
<br/>
<input onChange={handleChange} accept="image/jpeg,image/jpg,image/png" type="file" name="image" id="image" />
</div>
{(props.isEditing) &&
<div className="form-group custom-control custom-switch">
<input onChange={(event) => handleChange(event, true)} type="checkbox" name="isOnline" checked={inputState.isOnline} className="custom-control-input" id="isOnline" />
<label className="custom-control-label" htmlFor="isOnline">isOnline</label>
</div>
}
<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 AddEditFunction;

View File

@ -0,0 +1,46 @@
import { useState } from 'react';
import 'suneditor/dist/css/suneditor.min.css';
import dynamic from 'next/dynamic';
import htmlParser from 'html-react-parser';
import { complex } from '../utils/sunEditorConfig';
import api from '../utils/api';
import 'notyf/notyf.min.css'; // for React and Vue
const SunEditor = dynamic(
() => import('suneditor-react'),
{ ssr: false }
);
const EditArticleFunction = (props) => {
const [htmlContent, setHtmlContent] = useState("");
const handleEditorChange = (content) => {
setHtmlContent(content);
}
const handleSave = async (content) => {
let Notyf;
if (typeof window != 'undefined') {
Notyf = require('notyf');
}
const notyf = new Notyf.Notyf({
duration: 5000
});
try {
await api.put(`/admin/functions/article/${props.functionInfo.id}`, { article: content }, { headers: { 'Authorization': props.user.token } });
notyf.success('Sauvegardé!');
} catch {}
}
return (
<div className="container-fluid">
<SunEditor setContents={props.functionInfo.article} lang="fr" onChange={handleEditorChange} setOptions={{ buttonList: complex, callBackSave: handleSave }} />
<div className="container-fluid">
{htmlParser(htmlContent)}
</div>
</div>
);
}
export default EditArticleFunction;

View File

@ -1,7 +1,16 @@
import htmlParser from 'html-react-parser';
import FunctionTabs from './FunctionTabs/FunctionTabs'; import FunctionTabs from './FunctionTabs/FunctionTabs';
import FunctionForm from './FunctionForm'; import FunctionForm from './FunctionForm';
import FunctionComments from './FunctionComments/FunctionComments'; import FunctionComments from './FunctionComments/FunctionComments';
const Article = ({ article }) => {
return (
<div className="container-fluid">
{(article != undefined) && htmlParser(article)}
</div>
);
}
const FunctionTabManager = (props) => { const FunctionTabManager = (props) => {
if (props.type === "form") { if (props.type === "form") {
return ( return (
@ -9,7 +18,9 @@ const FunctionTabManager = (props) => {
<div className="FunctionComponent__slide"> <div className="FunctionComponent__slide">
<FunctionForm inputArray={ [...props.utilizationForm || []] } slug={props.slug} /> <FunctionForm inputArray={ [...props.utilizationForm || []] } slug={props.slug} />
</div> </div>
<div className="FunctionComponent__slide text-center">Article</div> <div className="FunctionComponent__slide">
<Article article={props.article} />
</div>
<div className="FunctionComponent__slide"> <div className="FunctionComponent__slide">
<FunctionComments functionId={props.id} /> <FunctionComments functionId={props.id} />
</div> </div>
@ -19,7 +30,9 @@ const FunctionTabManager = (props) => {
return ( return (
<FunctionTabs type={props.type}> <FunctionTabs type={props.type}>
<div className="FunctionComponent__slide text-center">Article</div> <div className="FunctionComponent__slide">
<Article article={props.article} />
</div>
<div className="FunctionComponent__slide"> <div className="FunctionComponent__slide">
<FunctionComments functionId={props.id} /> <FunctionComments functionId={props.id} />
</div> </div>

View File

@ -5862,6 +5862,11 @@
"sort-keys": "^1.0.0" "sort-keys": "^1.0.0"
} }
}, },
"notyf": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/notyf/-/notyf-3.6.0.tgz",
"integrity": "sha512-TqirJJOj5xTrkUGbat94aeFdBCMuJqlxvFAeySV08WRhcZDVzegIsQrYgek1ZN/Vhc+Ux3BgUmndS4ii1W2TJg=="
},
"nprogress": { "nprogress": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
@ -9537,6 +9542,20 @@
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
}, },
"suneditor": {
"version": "2.28.4",
"resolved": "https://registry.npmjs.org/suneditor/-/suneditor-2.28.4.tgz",
"integrity": "sha512-KLmKMq1QrBT9GT+/yv0d/a6YGEQ5+QUSmzZYJaIXGJ8QNNRV0BqfQP0gBnWY8s35HFKiU5meXWuzrI03AmubRg=="
},
"suneditor-react": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/suneditor-react/-/suneditor-react-2.9.1.tgz",
"integrity": "sha512-O26aLQ4dRWbdJv+d78pPaL/mLwWgVMiz64uo531+kHjBlNlREpV2dvvIYVH7TNxKi6LboeDZybJ5rm8kPMchAQ==",
"requires": {
"prop-types": "^15.7.2",
"suneditor": "^2.28.4"
}
},
"supports-color": { "supports-color": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",

View File

@ -18,12 +18,14 @@
"html-react-parser": "^0.10.2", "html-react-parser": "^0.10.2",
"next": "9.3.2", "next": "9.3.2",
"next-fonts": "^1.0.3", "next-fonts": "^1.0.3",
"notyf": "^3.6.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"react": "16.13.0", "react": "16.13.0",
"react-color": "^2.18.0", "react-color": "^2.18.0",
"react-dom": "16.13.0", "react-dom": "16.13.0",
"react-swipeable-views": "^0.13.9", "react-swipeable-views": "^0.13.9",
"react-swipeable-views-utils": "^0.13.9", "react-swipeable-views-utils": "^0.13.9",
"suneditor-react": "^2.9.1",
"universal-cookie": "^4.0.3" "universal-cookie": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,31 +1,76 @@
import { Fragment } from 'react'; import { Fragment, useState } from 'react';
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import SwipeableViews from 'react-swipeable-views';
import HeadTag from '../../components/HeadTag'; import HeadTag from '../../components/HeadTag';
import AddEditFunction from '../../components/AddEditFunction';
import EditArticleFunction from '../../components/EditArticleFunction';
import redirect from '../../utils/redirect'; import redirect from '../../utils/redirect';
import api from '../../utils/api';
import { API_URL } from '../../utils/config';
import '../../components/FunctionTabs/FunctionTabs.css';
import '../../public/css/pages/admin.css';
const AdminFunctionComponent = (props) => { const AdminFunctionComponent = (props) => {
if (!props.user.isAdmin && typeof window != 'undefined') { const [slideIndex, setSlideIndex] = useState(0);
return redirect({}, '/404');
}
return ( return (
<Fragment> <Fragment>
<HeadTag /> <HeadTag title={props.functionInfo.title} description={props.functionInfo.description} image={API_URL + props.functionInfo.image} />
<p>{props.slug}</p>
<div className="container-fluid">
<div className="container">
<div className="row justify-content-center">
<ul className="FunctionTabs__nav">
<li className="FunctionTabs__nav-item">
<a onClick={() => setSlideIndex(0)} className={`FunctionTabs__nav-link ${(slideIndex === 0) && "FunctionTabs__nav-link-active"}`}> Modifier</a>
</li>
<li className="FunctionTabs__nav-item">
<a onClick={() => setSlideIndex(1)} className={`FunctionTabs__nav-link ${(slideIndex === 1) && "FunctionTabs__nav-link-active"}`}>📝 Article</a>
</li>
<li className="FunctionTabs__nav-item">
<a onClick={() => setSlideIndex(2)} className={`FunctionTabs__nav-link ${(slideIndex === 2) && "FunctionTabs__nav-link-active"}`}> Utilisation</a>
</li>
</ul>
</div>
</div>
<div className="container-fluid">
<SwipeableViews onChangeIndex={(index) => setSlideIndex(index)} index={slideIndex}>
<div className="Admin__Function-slide">
<AddEditFunction
defaultInputState={{ ...props.functionInfo }}
user={props.user}
isEditing
/>
</div>
<div className="Admin__Function-slide">
<EditArticleFunction { ...props } />
</div>
</SwipeableViews>
</div>
</div>
</Fragment> </Fragment>
); );
} }
export async function getServerSideProps({ req, params }) { export async function getServerSideProps(context) {
const cookies = new Cookies(req.headers.cookie); const cookies = new Cookies(context.req.headers.cookie);
const { slug } = params; const user = { ...cookies.get('user') };
return { const { slug } = context.params;
props: { if (!user.isAdmin) {
user: { ...cookies.get('user') }, return redirect(context, '/404');
slug }
} return api.get(`/admin/functions/${slug}`, { headers: { 'Authorization': user.token } })
}; .then((response) => {
return {
props: {
user,
functionInfo: response.data
}
};
})
.catch(() => redirect(context, '/404'));
} }
export default AdminFunctionComponent; export default AdminFunctionComponent;

View File

@ -1,67 +1,21 @@
import Link from 'next/link'; import Link from 'next/link';
import { Fragment, useState, useEffect } from 'react'; import { Fragment, useState } from 'react';
import Cookies from "universal-cookie"; import Cookies from "universal-cookie";
import HeadTag from '../../components/HeadTag'; import HeadTag from '../../components/HeadTag';
import FunctionsList from '../../components/FunctionsList/FunctionsList';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { faTimes } from '@fortawesome/free-solid-svg-icons';
import Modal from '../../components/Modal'; import Modal from '../../components/Modal';
import FunctionsList from '../../components/FunctionsList/FunctionsList';
import AddEditFunction from '../../components/AddEditFunction';
import redirect from '../../utils/redirect'; import redirect from '../../utils/redirect';
import htmlParser from 'html-react-parser';
import Loader from '../../components/Loader';
import useAPI from '../../hooks/useAPI';
import api from '../../utils/api';
import '../../public/css/pages/admin.css'; import '../../public/css/pages/admin.css';
const Admin = (props) => { const Admin = (props) => {
const [, categories] = useAPI('/categories');
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [inputState, setInputState] = useState({ type: 'form' });
const [message, setMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (categories.length > 0) {
handleChange({
target: {
name: "categorieId",
value: categories[0].id
}
});
}
}, [categories]);
const toggleModal = () => setIsOpen(!isOpen); const toggleModal = () => setIsOpen(!isOpen);
const handleChange = (event, isTypeCheck = false) => {
const inputStateNew = { ...inputState };
inputStateNew[event.target.name] = (event.target.files != undefined) ? event.target.files[0] : (isTypeCheck) ? event.target.checked : event.target.value;
setInputState(inputStateNew);
}
const handleSubmit = (event) => {
event.preventDefault();
setIsLoading(true);
const formData = new FormData();
formData.append('type', inputState.type);
formData.append('categorieId', inputState.categorieId);
formData.append('title', inputState.title);
formData.append('slug', inputState.slug);
formData.append('description', inputState.description);
formData.append('image', inputState.image);
api.post('/admin/functions', formData, { headers: { 'Authorization': props.user.token } })
.then((response) => {
setMessage(`<p class="form-success"><b>Succès:</b> ${response.data.message}</p>`);
setIsLoading(false);
})
.catch((error) => {
setMessage(`<p class="form-error"><b>Erreur:</b> ${error.response.data.message}</p>`);
setIsLoading(false);
});
}
if (!props.user.isAdmin && typeof window != 'undefined') { if (!props.user.isAdmin && typeof window != 'undefined') {
return redirect({}, '/404'); return redirect({}, '/404');
} }
@ -87,58 +41,7 @@ const Admin = (props) => {
</div> </div>
<div className="col-24"> <div className="col-24">
<form onSubmit={handleSubmit}> <AddEditFunction defaultInputState={{ type: 'form' }} { ...props } />
<div className="form-group">
<label className="form-label" htmlFor="title">Titre :</label>
<input value={inputState.title} onChange={handleChange} type="text" name="title" id="title" className="form-control" placeholder="(e.g : Nombre aléatoire)" />
</div>
<div className="form-group">
<label className="form-label" htmlFor="slug">Slug :</label>
<input value={inputState.slug} onChange={handleChange} type="text" name="slug" id="slug" className="form-control" placeholder="(e.g : randomNumber)" />
</div>
<div className="form-group">
<label className="form-label" htmlFor="description">Description :</label>
<textarea style={{ height: 'auto' }} value={inputState.biography} onChange={handleChange} name="description" id="description" className="form-control" rows="5"></textarea>
</div>
<div className="form-group">
<label className="form-label" htmlFor="type">Type :</label>
<select onChange={handleChange} name="type" id="type" className="form-control">
<option value="form">Formulaire</option>
<option value="article">Article</option>
<option value="page">Page</option>
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="categorieId">Catégorie :</label>
<select onChange={handleChange} name="categorieId" id="categorieId" className="form-control">
{categories.map((category) => (
<option key={category.id} value={category.id} className="Admin__Modal-select-option" style={{ backgroundColor: category.color }}>{category.name}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label" htmlFor="image">Image <em>(150x150 recommandé)</em> :</label>
<br/>
<input onChange={handleChange} accept="image/jpeg,image/jpg,image/png" type="file" name="image" id="image" />
</div>
<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>
</div> </div>
</div> </div>
</div> </div>

View File

@ -44,6 +44,9 @@ a:hover {
div[aria-hidden="true"] > * { div[aria-hidden="true"] > * {
display: none; display: none;
} }
.notyf__message {
font-family: sans-serif;
}
/* LOADING */ /* LOADING */
.isLoading { .isLoading {
@ -136,3 +139,66 @@ a, .important {
background-color: #343a40; background-color: #343a40;
border-color: #343a40; border-color: #343a40;
} }
.custom-control {
display: flex;
justify-content: center;
}
.custom-control-input {
position: absolute;
z-index: -1;
opacity: 0;
}
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-input:checked~.custom-control-label::before {
color: #fff;
border-color: #007bff;
background-color: #007bff;
}
.custom-switch .custom-control-label::before {
left: -2.25rem;
width: 1.75rem;
pointer-events: all;
border-radius: .5rem;
}
.custom-control-label::before {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #adb5bd solid 1px;
}
.custom-switch .custom-control-input:checked~.custom-control-label::after {
background-color: #fff;
-webkit-transform: translateX(.75rem);
transform: translateX(.75rem);
}
.custom-switch .custom-control-label::after {
top: calc(.25rem + 2px);
left: calc(-2.25rem + 2px);
width: calc(1rem - 4px);
height: calc(1rem - 4px);
background-color: #adb5bd;
border-radius: .5rem;
transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
}
.custom-control-label::after {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50%/50% 50%;
}

View File

@ -7,7 +7,7 @@
box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25); box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25);
border: 1px solid black; border: 1px solid black;
border-radius: 1rem; border-radius: 1rem;
margin-top: 30px; margin-top: 40px;
} }
.FunctionComponent__title { .FunctionComponent__title {
margin: 0; margin: 0;

View File

@ -30,3 +30,6 @@
.Admin__table-row { .Admin__table-row {
padding: 15px; padding: 15px;
} }
.Admin__Function-slide {
margin-top: 40px;
}

View File

@ -26,66 +26,3 @@
.Profile__Modal-top-container { .Profile__Modal-top-container {
margin: 20px 0; margin: 20px 0;
} }
.custom-control {
display: flex;
justify-content: center;
}
.custom-control-input {
position: absolute;
z-index: -1;
opacity: 0;
}
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-input:checked~.custom-control-label::before {
color: #fff;
border-color: #007bff;
background-color: #007bff;
}
.custom-switch .custom-control-label::before {
left: -2.25rem;
width: 1.75rem;
pointer-events: all;
border-radius: .5rem;
}
.custom-control-label::before {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #adb5bd solid 1px;
}
.custom-switch .custom-control-input:checked~.custom-control-label::after {
background-color: #fff;
-webkit-transform: translateX(.75rem);
transform: translateX(.75rem);
}
.custom-switch .custom-control-label::after {
top: calc(.25rem + 2px);
left: calc(-2.25rem + 2px);
width: calc(1rem - 4px);
height: calc(1rem - 4px);
background-color: #adb5bd;
border-radius: .5rem;
transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
transition: transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;
}
.custom-control-label::after {
position: absolute;
top: .25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50%/50% 50%;
}

View File

@ -0,0 +1,14 @@
export const complex = [
["undo", "redo"],
["font", "fontSize", "formatBlock"],
["bold", "underline", "italic", "strike", "subscript", "superscript"],
["removeFormat"],
"/",
["fontColor", "hiliteColor"],
["outdent", "indent"],
["align", "horizontalRule", "list", "table"],
["link", "image", "video"],
["fullScreen", "showBlocks", "codeView"],
["preview", "print"],
["save", "template"]
];