From 9c5d1fc06bb216b946d24aff4dc00a9d14b2e3bd Mon Sep 17 00:00:00 2001 From: Divlo Date: Wed, 15 Apr 2020 22:50:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20frontend:=20Modifier=20in?= =?UTF-8?q?fo=20et=20Article?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/components/AddEditFunction.js | 129 ++++++++++++++++++ website/components/EditArticleFunction.js | 46 +++++++ website/components/FunctionTabManager.js | 17 ++- website/package-lock.json | 19 +++ website/package.json | 2 + website/pages/admin/[slug].js | 75 ++++++++-- website/pages/admin/index.js | 105 +------------- website/public/css/general.css | 66 +++++++++ .../public/css/pages/FunctionComponent.css | 2 +- website/public/css/pages/admin.css | 3 + website/public/css/pages/profile.css | 63 --------- website/utils/sunEditorConfig.js | 14 ++ 12 files changed, 359 insertions(+), 182 deletions(-) create mode 100644 website/components/AddEditFunction.js create mode 100644 website/components/EditArticleFunction.js create mode 100644 website/utils/sunEditorConfig.js diff --git a/website/components/AddEditFunction.js b/website/components/AddEditFunction.js new file mode 100644 index 0000000..e05d3bd --- /dev/null +++ b/website/components/AddEditFunction.js @@ -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(`

Erreur: ${error.response.data.message}

`); + setIsLoading(false); + }); + } + + return ( + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + {(props.isEditing) && +
+ handleChange(event, true)} type="checkbox" name="isOnline" checked={inputState.isOnline} className="custom-control-input" id="isOnline" /> + +
+ } + +
+ +
+
+ +
+ { + (isLoading) ? + + : + htmlParser(message) + } +
+
+ ); +} + +export default AddEditFunction; \ No newline at end of file diff --git a/website/components/EditArticleFunction.js b/website/components/EditArticleFunction.js new file mode 100644 index 0000000..e8306e6 --- /dev/null +++ b/website/components/EditArticleFunction.js @@ -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 ( +
+ +
+ {htmlParser(htmlContent)} +
+
+ ); +} + +export default EditArticleFunction; \ No newline at end of file diff --git a/website/components/FunctionTabManager.js b/website/components/FunctionTabManager.js index b766399..7c62edf 100644 --- a/website/components/FunctionTabManager.js +++ b/website/components/FunctionTabManager.js @@ -1,7 +1,16 @@ +import htmlParser from 'html-react-parser'; import FunctionTabs from './FunctionTabs/FunctionTabs'; import FunctionForm from './FunctionForm'; import FunctionComments from './FunctionComments/FunctionComments'; +const Article = ({ article }) => { + return ( +
+ {(article != undefined) && htmlParser(article)} +
+ ); +} + const FunctionTabManager = (props) => { if (props.type === "form") { return ( @@ -9,7 +18,9 @@ const FunctionTabManager = (props) => {
-
Article
+
+
+
@@ -19,7 +30,9 @@ const FunctionTabManager = (props) => { return ( -
Article
+
+
+
diff --git a/website/package-lock.json b/website/package-lock.json index 58392bb..31c7d50 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -5862,6 +5862,11 @@ "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": { "version": "0.2.0", "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", "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", diff --git a/website/package.json b/website/package.json index d7a9876..cf167f1 100644 --- a/website/package.json +++ b/website/package.json @@ -18,12 +18,14 @@ "html-react-parser": "^0.10.2", "next": "9.3.2", "next-fonts": "^1.0.3", + "notyf": "^3.6.0", "nprogress": "^0.2.0", "react": "16.13.0", "react-color": "^2.18.0", "react-dom": "16.13.0", "react-swipeable-views": "^0.13.9", "react-swipeable-views-utils": "^0.13.9", + "suneditor-react": "^2.9.1", "universal-cookie": "^4.0.3" }, "devDependencies": { diff --git a/website/pages/admin/[slug].js b/website/pages/admin/[slug].js index 001b826..a1e520b 100644 --- a/website/pages/admin/[slug].js +++ b/website/pages/admin/[slug].js @@ -1,31 +1,76 @@ -import { Fragment } from 'react'; +import { Fragment, useState } from 'react'; import Cookies from "universal-cookie"; +import SwipeableViews from 'react-swipeable-views'; import HeadTag from '../../components/HeadTag'; +import AddEditFunction from '../../components/AddEditFunction'; +import EditArticleFunction from '../../components/EditArticleFunction'; 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) => { - if (!props.user.isAdmin && typeof window != 'undefined') { - return redirect({}, '/404'); - } + const [slideIndex, setSlideIndex] = useState(0); return ( - -

{props.slug}

+ + +
); } -export async function getServerSideProps({ req, params }) { - const cookies = new Cookies(req.headers.cookie); - const { slug } = params; - return { - props: { - user: { ...cookies.get('user') }, - slug - } - }; +export async function getServerSideProps(context) { + const cookies = new Cookies(context.req.headers.cookie); + const user = { ...cookies.get('user') }; + const { slug } = context.params; + if (!user.isAdmin) { + return redirect(context, '/404'); + } + 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; \ No newline at end of file diff --git a/website/pages/admin/index.js b/website/pages/admin/index.js index 1ecc462..2835443 100644 --- a/website/pages/admin/index.js +++ b/website/pages/admin/index.js @@ -1,67 +1,21 @@ import Link from 'next/link'; -import { Fragment, useState, useEffect } from 'react'; +import { Fragment, useState } from 'react'; import Cookies from "universal-cookie"; import HeadTag from '../../components/HeadTag'; -import FunctionsList from '../../components/FunctionsList/FunctionsList'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import Modal from '../../components/Modal'; +import FunctionsList from '../../components/FunctionsList/FunctionsList'; +import AddEditFunction from '../../components/AddEditFunction'; 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'; const Admin = (props) => { - const [, categories] = useAPI('/categories'); 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 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(`

Succès: ${response.data.message}

`); - setIsLoading(false); - }) - .catch((error) => { - setMessage(`

Erreur: ${error.response.data.message}

`); - setIsLoading(false); - }); - } - if (!props.user.isAdmin && typeof window != 'undefined') { return redirect({}, '/404'); } @@ -87,58 +41,7 @@ const Admin = (props) => {
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- -
- -
- -
-
-
- { - (isLoading) ? - - : - htmlParser(message) - } -
+
diff --git a/website/public/css/general.css b/website/public/css/general.css index 6895f94..2b7dfd8 100644 --- a/website/public/css/general.css +++ b/website/public/css/general.css @@ -44,6 +44,9 @@ a:hover { div[aria-hidden="true"] > * { display: none; } +.notyf__message { + font-family: sans-serif; +} /* LOADING */ .isLoading { @@ -135,4 +138,67 @@ a, .important { color: #fff; background-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%; } \ No newline at end of file diff --git a/website/public/css/pages/FunctionComponent.css b/website/public/css/pages/FunctionComponent.css index 67d17c8..189f291 100644 --- a/website/public/css/pages/FunctionComponent.css +++ b/website/public/css/pages/FunctionComponent.css @@ -7,7 +7,7 @@ box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25); border: 1px solid black; border-radius: 1rem; - margin-top: 30px; + margin-top: 40px; } .FunctionComponent__title { margin: 0; diff --git a/website/public/css/pages/admin.css b/website/public/css/pages/admin.css index 2ee2c2b..0272904 100644 --- a/website/public/css/pages/admin.css +++ b/website/public/css/pages/admin.css @@ -29,4 +29,7 @@ } .Admin__table-row { padding: 15px; +} +.Admin__Function-slide { + margin-top: 40px; } \ No newline at end of file diff --git a/website/public/css/pages/profile.css b/website/public/css/pages/profile.css index 9d205aa..b1708fc 100644 --- a/website/public/css/pages/profile.css +++ b/website/public/css/pages/profile.css @@ -25,67 +25,4 @@ } .Profile__Modal-top-container { 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%; } \ No newline at end of file diff --git a/website/utils/sunEditorConfig.js b/website/utils/sunEditorConfig.js new file mode 100644 index 0000000..ed86d68 --- /dev/null +++ b/website/utils/sunEditorConfig.js @@ -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"] +]; \ No newline at end of file