From 307dec2ca7171ea7ba184ab5eadc4043531c347c Mon Sep 17 00:00:00 2001 From: Giulia Ghisini Date: Thu, 22 Feb 2024 11:28:20 +0100 Subject: [PATCH] feat: added channel subscription block --- package.json | 70 +++++++++- .../Blocks/NewsletterSubscribe/Background.jsx | 26 ++++ .../Blocks/NewsletterSubscribe/Edit.jsx | 110 ++++++++++++++++ .../NewsletterSubscribe.scss | 93 +++++++++++++ .../Blocks/NewsletterSubscribe/Sidebar.jsx | 108 ++++++++++++++++ .../NewsletterSubscribe/SubscribeButton.jsx | 24 ++++ .../Blocks/NewsletterSubscribe/View.jsx | 65 ++++++++++ src/icons/subscribe.svg | 3 + src/index.js | 46 ++++--- src/views/Channel.jsx | 122 +++--------------- 10 files changed, 545 insertions(+), 122 deletions(-) create mode 100644 src/components/Blocks/NewsletterSubscribe/Background.jsx create mode 100644 src/components/Blocks/NewsletterSubscribe/Edit.jsx create mode 100644 src/components/Blocks/NewsletterSubscribe/NewsletterSubscribe.scss create mode 100644 src/components/Blocks/NewsletterSubscribe/Sidebar.jsx create mode 100644 src/components/Blocks/NewsletterSubscribe/SubscribeButton.jsx create mode 100644 src/components/Blocks/NewsletterSubscribe/View.jsx create mode 100644 src/icons/subscribe.svg diff --git a/package.json b/package.json index 330e49d..6c883fd 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,63 @@ "pre-commit": "lint-staged" } }, + "stylelint": { + "extends": [ + "stylelint-config-idiomatic-order" + ], + "plugins": [ + "stylelint-prettier" + ], + "overrides": [ + { + "files": [ + "**/*.scss" + ], + "customSyntax": "postcss-scss" + }, + { + "files": [ + "**/*.less" + ], + "customSyntax": "postcss-less" + }, + { + "files": [ + "**/*.overrides" + ], + "customSyntax": "postcss-less" + } + ], + "rules": { + "prettier/prettier": true, + "rule-empty-line-before": [ + "always-multi-line", + { + "except": [ + "first-nested" + ], + "ignore": [ + "after-comment" + ] + } + ] + } + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json}": [ + "npx eslint --max-warnings=0 --fix", + "npx prettier --single-quote --write" + ], + "src/**/*.{css,less}": [ + "npx stylelint --fix" + ], + "src/**/*.scss": [ + "npx stylelint --fix --customSyntax postcss-scss" + ], + "src/**/*.overrides": [ + "npx stylelint --fix --syntax less" + ] + }, "prettier": { "singleQuote": true, "trailingComma": "all" @@ -34,7 +91,18 @@ "@commitlint/config-conventional": "^12.1.4", "@plone/scripts": "*", "@release-it/conventional-changelog": "^2.0.1", + "stylelint": "15.11.0", + "stylelint-config-idiomatic-order": "9.0.0", + "stylelint-prettier": "4.0.2", "husky": "^6.0.0", - "release-it": "^14.10.1" + "release-it": "^14.10.1", + "jest-css-modules": "^2.1.0", + "@babel/eslint-parser": "7.23.3", + "@babel/plugin-proposal-export-default-from": "7.18.10" + }, + "peerDependencies": { + "@plone/volto": ">=16.0.0-alpha.38", + "design-comuni-plone-theme": "*", + "design-reack-kit": "*" } } diff --git a/src/components/Blocks/NewsletterSubscribe/Background.jsx b/src/components/Blocks/NewsletterSubscribe/Background.jsx new file mode 100644 index 0000000..29836cd --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/Background.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import cx from 'classnames'; +import { addAppURL, flattenToAppURL } from '@plone/volto/helpers'; + +const Background = ({ data, children }) => { + return ( +
+ {data.background?.[0] && ( +
+ )} + {children} +
+ ); +}; + +export default Background; diff --git a/src/components/Blocks/NewsletterSubscribe/Edit.jsx b/src/components/Blocks/NewsletterSubscribe/Edit.jsx new file mode 100644 index 0000000..9ed8e70 --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/Edit.jsx @@ -0,0 +1,110 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { defineMessages, useIntl } from 'react-intl'; +import { Container } from 'design-react-kit'; +import { SidebarPortal } from '@plone/volto/components'; +import { flattenToAppURL } from '@plone/volto/helpers'; +import { getContent } from '@plone/volto/actions'; +import { TextEditorWidget } from 'design-comuni-plone-theme/components/ItaliaTheme'; +import { useHandleDetachedBlockFocus } from 'design-comuni-plone-theme/helpers/blocks'; +import Sidebar from 'volto-newsletter/components/Blocks/NewsletterSubscribe/Sidebar'; +import Background from 'volto-newsletter/components/Blocks/NewsletterSubscribe/Background'; +import SubscribeButton from 'volto-newsletter/components/Blocks/NewsletterSubscribe/SubscribeButton'; +import './NewsletterSubscribe.scss'; + +const messages = defineMessages({ + text: { + id: 'Insert text…', + defaultMessage: 'Insert text…', + }, + selectChannel: { + id: 'Newsletter Block: Select a channel from right sidebar', + defaultMessage: 'Blocco Newsletter Block: Seleziona un canale dalla barra a destra', + }, + unsubscribable_channel: { + id: 'Newsletter Block: unsubscribable_channel', + defaultMessage: 'In canale selezionato non è sottoscrivibile.', + }, +}); + +const Edit = (props) => { + const { data, block, selected, ...otherProps } = props; + const intl = useIntl(); + const { selectedField, setSelectedField } = useHandleDetachedBlockFocus(props, 'text'); + + const dispatch = useDispatch(); + const channelID = data.channel?.[0] ? flattenToAppURL(data.channel[0]['@id']) : null; + const channel = useSelector((state) => state.content.subrequests['channel_' + channelID]?.data); + console.log(channel); + useEffect(() => { + //load channel data + if (!channel && channelID) { + dispatch(getContent(channelID, null, 'channel_' + channelID)); + } + }, [data.channel]); + + return ( + <> + {channel && channel.is_subscribable ? ( +
+ + +
+ { + setSelectedField('text'); + }} + /> +
+
+ { + setSelectedField('title'); + }} + /> +
+ +
+
+
+ ) : channel && !channel.is_subscribable ? ( +
{intl.formatMessage(messages.unsubscribable_channel)}
+ ) : ( +
{intl.formatMessage(messages.selectChannel)}
+ )} + + + + + ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +Edit.propTypes = { + data: PropTypes.objectOf(PropTypes.any).isRequired, + id: PropTypes.string.isRequired, + selected: PropTypes.bool.isRequired, + block: PropTypes.string.isRequired, +}; + +export default Edit; diff --git a/src/components/Blocks/NewsletterSubscribe/NewsletterSubscribe.scss b/src/components/Blocks/NewsletterSubscribe/NewsletterSubscribe.scss new file mode 100644 index 0000000..5bc24e6 --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/NewsletterSubscribe.scss @@ -0,0 +1,93 @@ +.block.newsletter-subscribe { + .edit-infos { + font-style: italic; + text-align: center; + padding: 1rem; + } + .block-content { + position: relative; + height: auto; + padding: 2.5rem 0; + text-align: center; + background-color: var(--bs-gray-600); + color: var(--bs-white); + z-index: 0; + + &.with-bg { + color: var(--bs-white); + + h2, + h3, + h4, + h5, + h6, + a { + color: var(--bs-white); + } + } + + @media (max-width: 992px) { + /*lg breakpoint*/ + padding: 2rem 1rem; + } + } + + .background-image { + position: absolute; + z-index: -1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-position: top center; + background-repeat: no-repeat; + background-size: cover; + + &:after { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(#000, 0.65); + content: ''; + left: 0; + top: 0; + } + } + + .title, + .text { + p:last-of-type { + margin-bottom: 0; + } + + h1:last-child, + h2:last-child, + h3:last-child, + h4:last-child, + h5:last-child { + margin-bottom: 0; + } + + h1:first-child, + h2:first-child, + h3:first-child, + h4:first-child, + h5:first-child { + margin-top: 0; + } + } + .block-text-content { + z-index: 0; + } + + .slate-editor { + [data-slate-placeholder='true'] { + opacity: 0.7 !important; + } + } + + .title, + .text { + margin-bottom: 2rem; + } +} diff --git a/src/components/Blocks/NewsletterSubscribe/Sidebar.jsx b/src/components/Blocks/NewsletterSubscribe/Sidebar.jsx new file mode 100644 index 0000000..08e13d4 --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/Sidebar.jsx @@ -0,0 +1,108 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { Segment } from 'semantic-ui-react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { ObjectBrowserWidget, CheckboxWidget, TextWidget } from '@plone/volto/components'; + +const messages = defineMessages({ + channel: { + id: 'channel', + defaultMessage: 'Canale', + }, + backgroundImage: { + id: 'backgroundImage', + defaultMessage: 'Immagine di sfondo', + }, + showFullWidth: { + id: 'show_full_width', + defaultMessage: 'A tutta larghezza', + }, + buttonText: { + id: 'newsletter_subscribe_cta', + defaultMessage: 'Testo del bottone', + }, +}); + +const Sidebar = ({ block, data, onChangeBlock, required }) => { + const intl = useIntl(); + + useEffect(() => { + //set default values + onChangeBlock(block, { + ...data, + showFullWidth: data.showFullWidth === undefined ? true : data.showFullWidth, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + +
+

+ +

+
+
+ onChangeBlock(block, { ...data, [id]: value })} + /> + + onChangeBlock(block, { ...data, [id]: value })} + /> + { + onChangeBlock(block, { ...data, [name]: checked }); + }} + /> + + { + onChangeBlock(block, { + ...data, + [name]: value, + }); + }} + /> +
+
+ ); +}; + +Sidebar.propTypes = { + block: PropTypes.string.isRequired, + data: PropTypes.objectOf(PropTypes.any).isRequired, + onChangeBlock: PropTypes.func.isRequired, + openObjectBrowser: PropTypes.func.isRequired, + required: PropTypes.bool, +}; + +export default Sidebar; diff --git a/src/components/Blocks/NewsletterSubscribe/SubscribeButton.jsx b/src/components/Blocks/NewsletterSubscribe/SubscribeButton.jsx new file mode 100644 index 0000000..f7b06a5 --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/SubscribeButton.jsx @@ -0,0 +1,24 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { defineMessages, useIntl } from 'react-intl'; +import { UniversalLink } from '@plone/volto/components'; +import { Button } from 'design-react-kit'; + +const messages = defineMessages({ + subscribe: { + id: 'Newsletter block: subscribe', + defaultMessage: 'Iscriviti', + }, +}); + +const SubscribeButton = ({ channel, data, inEditMode = false }) => { + const text = data.buttonText ?? intl.formatMessage(messages.subscribe); + return ( + + ); +}; + +export default SubscribeButton; diff --git a/src/components/Blocks/NewsletterSubscribe/View.jsx b/src/components/Blocks/NewsletterSubscribe/View.jsx new file mode 100644 index 0000000..d5e8ddf --- /dev/null +++ b/src/components/Blocks/NewsletterSubscribe/View.jsx @@ -0,0 +1,65 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import { Container } from 'design-react-kit'; +import { TextBlockView } from '@plone/volto-slate/blocks/Text'; +import { flattenToAppURL } from '@plone/volto/helpers'; +import { getContent } from '@plone/volto/actions'; +import { checkRichTextHasContent } from 'design-comuni-plone-theme/helpers'; +import Background from 'volto-newsletter/components/Blocks/NewsletterSubscribe/Background'; +import SubscribeButton from 'volto-newsletter/components/Blocks/NewsletterSubscribe/SubscribeButton'; + +import './NewsletterSubscribe.scss'; + +const View = ({ data, id }) => { + const intl = useIntl(); + const dispatch = useDispatch(); + const channelID = data.channel?.[0] ? flattenToAppURL(data.channel[0]['@id']) : null; + const channel = useSelector((state) => state.content.subrequests['channel_' + channelID]?.data); + + useEffect(() => { + //load channel data + if (!channel && channelID) { + dispatch(getContent(channelID, null, 'channel_' + channelID)); + } + }, [data.channel]); + + return channel && channel.is_subscribable ? ( +
+
+ + + {checkRichTextHasContent(data.title) && ( +
+ +
+ )} + + {checkRichTextHasContent(data.text) && ( +
+ +
+ )} + + +
+
+
+
+ ) : ( + <> + ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +View.propTypes = { + data: PropTypes.objectOf(PropTypes.any).isRequired, + id: PropTypes.string.isRequired, +}; + +export default View; diff --git a/src/icons/subscribe.svg b/src/icons/subscribe.svg new file mode 100644 index 0000000..67ed780 --- /dev/null +++ b/src/icons/subscribe.svg @@ -0,0 +1,3 @@ + + Subscribe + \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0e5c30a..dbb000f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,4 @@ -export { - subscribeNewsletter, - resetSubscribeNewsletter, - unsubscribeNewsletter, - resetUnsubscribeNewsletter, - confirmNewsletterSubscription, - resetConfirmNewsletterSubscription, -} from './actions'; +export { subscribeNewsletter, resetSubscribeNewsletter, unsubscribeNewsletter, resetUnsubscribeNewsletter, confirmNewsletterSubscription, resetConfirmNewsletterSubscription } from './actions'; import Channel from './views/Channel'; import Message from './views/Message'; @@ -13,11 +6,11 @@ import NewsletterConfirmSubscribe from './views/NewsletterConfirmSubscribe'; import NewsletterConfirmUnsubscribe from './views/NewsletterConfirmUnsubscribe'; import NewsletterConfirmView from './views/NewsletterConfirmView'; -import { - subscribeNewsletterReducer, - unsubscribeNewsletterReducer, - confirmNewsletterSubscriptionReducer, -} from './reducers'; +import subscribeSVG from './icons/subscribe.svg'; +import NewsletterSubscribeView from 'volto-newsletter/components/Blocks/NewsletterSubscribe/View'; +import NewsletterSubscribeEdit from 'volto-newsletter/components/Blocks/NewsletterSubscribe/Edit'; + +import { subscribeNewsletterReducer, unsubscribeNewsletterReducer, confirmNewsletterSubscriptionReducer } from './reducers'; const applyConfig = (config) => { config.addonReducers = { @@ -41,14 +34,29 @@ const applyConfig = (config) => { Message, }; + config.blocks.blocksConfig = { + ...config.blocks.blocksConfig, + 'newsletter-subscribe': { + id: 'newsletter-subscribe', + title: 'Iscrizione newsletter', + icon: subscribeSVG, + group: 'newsletter', + view: NewsletterSubscribeView, + edit: NewsletterSubscribeEdit, + restricted: false, + mostUsed: false, + security: { + addPermission: [], + view: [], + }, + sidebarTab: 1, + }, + }; + config.blocks.groupBlocksOrder.push({ id: 'newsletter', title: 'Newsletter' }); + return config; }; export default applyConfig; -export { - Channel, - NewsletterConfirmSubscribe, - NewsletterConfirmUnsubscribe, - NewsletterConfirmView, -}; +export { Channel, NewsletterConfirmSubscribe, NewsletterConfirmUnsubscribe, NewsletterConfirmView }; diff --git a/src/views/Channel.jsx b/src/views/Channel.jsx index 363c4f3..a884c58 100644 --- a/src/views/Channel.jsx +++ b/src/views/Channel.jsx @@ -1,22 +1,11 @@ import React, { useState, createRef, useCallback, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { Grid } from 'semantic-ui-react'; import { Form, Label, Input, Button } from 'design-react-kit'; import { useDispatch, useSelector } from 'react-redux'; import { toast } from 'react-toastify'; import { Icon } from 'design-comuni-plone-theme/components/ItaliaTheme'; -import { - SideMenu, - PageHeader, - RichTextSection, - SkipToMainContent, -} from 'design-comuni-plone-theme/components/ItaliaTheme/View'; -import { - subscribeNewsletter, - resetSubscribeNewsletter, - unsubscribeNewsletter, - resetUnsubscribeNewsletter, -} from '../actions'; +import { SideMenu, PageHeader, RichTextSection, SkipToMainContent } from 'design-comuni-plone-theme/components/ItaliaTheme/View'; +import { subscribeNewsletter, resetSubscribeNewsletter, unsubscribeNewsletter, resetUnsubscribeNewsletter } from '../actions'; import HoneypotWidget from './HoneypotWidget/HoneypotWidget'; const messages = defineMessages({ @@ -42,18 +31,15 @@ const messages = defineMessages({ }, newsletterSubscriptionThankyou: { id: 'newsletterSubscriptionThankyou', - defaultMessage: - 'Grazie per esserti iscritto alla newsletter. Controlla la tua casella di posta elettronica per verificare la tua iscrizione.', + defaultMessage: 'Grazie per esserti iscritto alla newsletter. Controlla la tua casella di posta elettronica per verificare la tua iscrizione.', }, newsletterUnsubscriptionConfirmation: { id: 'newsletterUnsubscriptionConfirmation', - defaultMessage: - "La tua richiesta di cancellazione dell'iscrizione alla newsletter è stata inviata. Controlla la tua casella di posta elettronica per verificare l'operazione.", + defaultMessage: "La tua richiesta di cancellazione dell'iscrizione alla newsletter è stata inviata. Controlla la tua casella di posta elettronica per verificare l'operazione.", }, newsletterSubscriptionError: { id: 'newsletterSubscriptionError', - defaultMessage: - "Si è verificato un errore durante l'invio della richiesta. Si prega di riprovare più tardi.", + defaultMessage: "Si è verificato un errore durante l'invio della richiesta. Si prega di riprovare più tardi.", }, user_subscribe_success: { id: 'user_subscribe_success', @@ -77,8 +63,7 @@ const messages = defineMessages({ }, unsubscribe_generic: { id: 'unsubscribe_generic', - defaultMessage: - 'Errore durante la richiesta. Si prega di riprovare più tardi.', + defaultMessage: 'Errore durante la richiesta. Si prega di riprovare più tardi.', }, }); @@ -86,23 +71,11 @@ const Channel = ({ content, location }) => { const intl = useIntl(); const dispatch = useDispatch(); - const { - loading: subscribeLoading, - loaded: subscribeLoaded, - error: subscribeError, - result: subscribeResult, - } = useSelector((state) => state.subscribeNewsletter); - const { status: subscribeStatus, errors: subscribeErrors = [] } = - subscribeResult ?? {}; + const { loading: subscribeLoading, loaded: subscribeLoaded, error: subscribeError, result: subscribeResult } = useSelector((state) => state.subscribeNewsletter); + const { status: subscribeStatus, errors: subscribeErrors = [] } = subscribeResult ?? {}; - const { - loading: unsubscribeLoading, - loaded: unsubscribeLoaded, - error: unsubscribeError, - result: unsubscribeResult, - } = useSelector((state) => state.unsubscribeNewsletter); - const { status: unsubscribeStatus, errors: unsubscribeErrors = [] } = - unsubscribeResult ?? {}; + const { loading: unsubscribeLoading, loaded: unsubscribeLoaded, error: unsubscribeError, result: unsubscribeResult } = useSelector((state) => state.unsubscribeNewsletter); + const { status: unsubscribeStatus, errors: unsubscribeErrors = [] } = unsubscribeResult ?? {}; const [email, setEmail] = useState(''); const [unsubEmail, setUnsubEmail] = useState(''); @@ -238,37 +211,17 @@ const Channel = ({ content, location }) => { <>
- +
- -
+ +
{content.is_subscribable && (

{intl.formatMessage(messages.subscribeNewsletterLabel)}

- {subscribeLoaded && subscribeStatus !== 'error' && ( -

- {intl.formatMessage( - messages.newsletterSubscriptionThankyou, - )} -

- )} + {subscribeLoaded && subscribeStatus !== 'error' &&

{intl.formatMessage(messages.newsletterSubscriptionThankyou)}

}
{ }} field={fieldHoney} /> - @@ -333,23 +274,12 @@ const Channel = ({ content, location }) => {
)} - +

{intl.formatMessage(messages.unsubscribeNewsletterLabel)}

- {unsubscribeLoaded && unsubscribeStatus !== 'error' && ( -

- {intl.formatMessage( - messages.newsletterUnsubscriptionConfirmation, - )} -

- )} + {unsubscribeLoaded && unsubscribeStatus !== 'error' &&

{intl.formatMessage(messages.newsletterUnsubscriptionConfirmation)}

}
{ }} field={fieldHoney} /> -