diff --git a/CHANGELOG.MD b/CHANGELOG.MD index cbf474fd3..f4f7fe364 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,10 @@ +## September 3, 2024 +- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668) + - Implemented authoring side nav + - Implemented authoring bottom nav + - Implemented authoring section context + - Added skeletons for the required sections + ## August 28, 2024 - **Bugfix** Fix keyboard focus being trapped on buttons on authoring page [🎟️ DESENG-687](https://citz-gdx.atlassian.net/browse/DESENG-687) diff --git a/met-web/src/assets/images/pagePreview.png b/met-web/src/assets/images/pagePreview.png new file mode 100644 index 000000000..9dcd251e4 Binary files /dev/null and b/met-web/src/assets/images/pagePreview.png differ diff --git a/met-web/src/components/common/RichTextEditor/index.tsx b/met-web/src/components/common/RichTextEditor/index.tsx index 9c2bbf3f9..ce28d24d2 100644 --- a/met-web/src/components/common/RichTextEditor/index.tsx +++ b/met-web/src/components/common/RichTextEditor/index.tsx @@ -67,7 +67,6 @@ const RichTextEditor = ({ onEditorStateChange={handleChange} handlePastedText={() => false} editorStyle={{ - height: '10em', padding: '1em', resize: 'vertical', }} diff --git a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx index b1f6df846..5656e367b 100644 --- a/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx +++ b/met-web/src/components/engagement/admin/config/EngagementUpdateAction.tsx @@ -58,7 +58,7 @@ export const engagementUpdateAction: ActionFunction = async ({ request, params } console.error('Error updating team members', e); } - return redirect(`/engagements/${engagementId}/view`); + return redirect(`/engagements/${engagementId}/details/config`); }; export default engagementUpdateAction; diff --git a/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx index 3929b4ab1..1ee93f000 100644 --- a/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx +++ b/met-web/src/components/engagement/admin/config/wizard/ConfigWizard.tsx @@ -82,7 +82,7 @@ const ConfigForm = () => { }), { method: 'patch', - action: `/engagements/${engagement.id}/config/`, + action: `/engagements/${engagement.id}/details/config/edit`, }, ); }; @@ -131,7 +131,7 @@ const ConfigForm = () => { > - We're just looking over your configuration. + We're saving your configuration. diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx new file mode 100644 index 000000000..e94300817 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBanner.tsx @@ -0,0 +1,313 @@ +import { FormControlLabel, Grid, MenuItem, Radio, RadioGroup, Select } from '@mui/material'; +import React, { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { TextField } from 'components/common/Input'; +import { AuthoringTemplateOutletContext } from './types'; +import { colors } from 'styles/Theme'; +import { EyebrowText as FormDescriptionText } from 'components/common/Typography'; +import { MetLabel, MetHeader3, MetLabel as MetBigLabel } from 'components/common'; +import ImageUpload from 'components/imageUpload'; + +const ENGAGEMENT_UPLOADER_HEIGHT = '360px'; +const ENGAGEMENT_CROPPER_ASPECT_RATIO = 1920 / 700; + +const AuthoringBanner = () => { + const { setValue, watch, control, engagement }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + + const [bannerImage, setBannerImage] = useState(); + const [savedBannerImageFileName, setSavedBannerImageFileName] = useState(engagement.banner_filename || ''); + + const [openCtaExternalURLEnabled, setOpenCtaExternalURLEnabled] = useState(false); + const [openCtaSectionSelectEnabled, setOpenCtaSectionSelectEnabled] = useState(true); + const [viewResultsCtaExternalURLEnabled, setViewResultsCtaExternalURLEnabled] = useState(false); + const [viewResultsSectionSelectEnabled, setViewResultsSectionSelectEnabled] = useState(true); + + //Define the styles + const metBigLabelStyles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + lineHeight: 1.167, + color: '#292929', + fontWeight: '700', + }; + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const formDescriptionTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + // boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + '&:disabled': { + boxShadow: 'none', + }, + }; + + const handleAddBannerImage = (files: File[]) => { + if (files.length > 0) { + setBannerImage(files[0]); + return; + } + setBannerImage(null); + setSavedBannerImageFileName(''); + }; + + const handleRadioSelectChange = (event: React.FormEvent, source: string) => { + const newRadioValue = event.currentTarget.value; + if ('ctaLinkType' === source) { + setOpenCtaExternalURLEnabled('section' === newRadioValue ? false : true); + setOpenCtaSectionSelectEnabled('section' === newRadioValue ? true : false); + } else { + setViewResultsCtaExternalURLEnabled('section' === newRadioValue ? false : true); + setViewResultsSectionSelectEnabled('section' === newRadioValue ? true : false); + } + }; + + return ( + + + + + + + + + + + + Hero Image (Required) + + + Please ensure you use high quality images that help to communicate the topic of your engagement. You + must ensure that any important subject matter is positioned on the right side. + + + + + + Engagement State Content Variants + + The content in this section of your engagement may be changed based on the state or status of your + engagement. Select the Section Preview or Page Preview button to see each of these states. + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AuthoringBanner; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx new file mode 100644 index 000000000..5b167bd8d --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringBottomNav.tsx @@ -0,0 +1,209 @@ +import React from 'react'; +import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material'; +import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme'; +import { When, Unless } from 'react-if'; +import { BodyText } from 'components/common/Typography'; +import { elevations } from 'components/common'; +import { Button } from 'components/common/Input'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck } from '@fortawesome/pro-regular-svg-icons'; +import { StatusCircle } from '../../view/AuthoringTab'; +import pagePreview from 'assets/images/pagePreview.png'; +import { AuthoringBottomNavProps, LanguageSelectorProps } from './types'; +import { getLanguageValue } from './AuthoringTemplate'; + +const AuthoringBottomNav = ({ + isDirty, + isValid, + isSubmitting, + currentLanguage, + setCurrentLanguage, + languages, + pageTitle, +}: AuthoringBottomNavProps) => { + const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); + const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' }; + + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + return ( + + + + + Currently Authoring + + {pageTitle} + + {'\u2B24'} + + {getLanguageValue(currentLanguage, languages)} + + + + + + + + + + + + + + + ); +}; + +const LanguageSelector = ({ + currentLanguage, + setCurrentLanguage, + languages, + isDirty, + isSubmitting, +}: LanguageSelectorProps) => { + const handleSelectChange = (event: SelectChangeEvent) => { + const newLanguageCode = event.target.value; + if (isDirty && !isSubmitting) + // todo: Replace this message with our stylized modal message. + window.confirm( + `Are you sure you want to switch to ${ + getLanguageValue(newLanguageCode, languages) || 'another language' + }? You have unsaved changes for the ${ + getLanguageValue(currentLanguage, languages) || 'current' + } language.`, + ); + setCurrentLanguage(newLanguageCode); + }; + return ( + + ); +}; + +export default AuthoringBottomNav; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx new file mode 100644 index 000000000..e80e48f16 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringContext.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import { FormProvider, useForm } from 'react-hook-form'; +import { createSearchParams, useFetcher, Outlet } from 'react-router-dom'; + +export interface EngagementUpdateData { + id: number; + status_id: number; + taxon_id: number; + content_id: number; + name: string; + start_date: Dayjs; + end_date: Dayjs; + description: string; + rich_description: string; + banner_filename: string; + status_block: string[]; + title: string; + icon_name: string; + metadata_value: string; + send_report: boolean; + slug: string; + request_type: string; +} + +export const AuthoringContext = () => { + const fetcher = useFetcher(); + const locationArray = window.location.href.split('/'); + const slug = locationArray[locationArray.length - 1]; + const defaultDateValue = dayjs(new Date(1970, 0, 1)); + const engagementUpdateForm = useForm({ + defaultValues: { + id: 0, + status_id: 0, + taxon_id: 0, + content_id: 0, + name: '', + start_date: defaultDateValue, + end_date: defaultDateValue, + description: '', + rich_description: '', + banner_filename: '', + status_block: [], + title: '', + icon_name: '', + metadata_value: '', + send_report: undefined, + slug: '', + request_type: '', + }, + mode: 'onSubmit', + reValidateMode: 'onChange', + }); + const onSubmit = async (data: EngagementUpdateData) => { + fetcher.submit( + createSearchParams({ + id: 0 === data.id ? '' : data.id.toString(), + status_id: 0 === data.status_id ? '' : data.status_id.toString(), + taxon_id: 0 === data.taxon_id ? '' : data.taxon_id.toString(), + content_id: 0 === data.content_id ? '' : data.content_id.toString(), + name: data.name, + start_date: + '1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.start_date.format('YYYY-MM-DD'), + end_date: + '1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.end_date.format('YYYY-MM-DD'), + description: data.description, + rich_description: data.rich_description, + banner_filename: data.banner_filename, + status_block: data.status_block, + title: data.title, + icon_name: data.icon_name, + metadata_value: data.metadata_value, + send_report: getSendReportValue(data.send_report), + slug: data.slug, + request_type: data.request_type, + }), + { + method: 'post', + action: `/engagements/${data.id}/details/authoring/${slug}`, + }, + ); + }; + + const getSendReportValue = (valueToInterpret: boolean) => { + if (undefined === valueToInterpret) { + return ''; + } + return valueToInterpret ? 'true' : 'false'; + }; + + return ( + + + + ); +}; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx new file mode 100644 index 000000000..408101a1f --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringDetails.tsx @@ -0,0 +1,472 @@ +import { Grid, IconButton, MenuItem, Select, SelectChangeEvent, Tab, Tabs } from '@mui/material'; +import { EyebrowText as FormDescriptionText } from 'components/common/Typography'; +import { colors, MetLabel, MetHeader3, MetLabel as MetBigLabel } from 'components/common'; +import { Button, TextField } from 'components/common/Input'; +import React, { SyntheticEvent, useState } from 'react'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { AuthoringTemplateOutletContext, DetailsTabProps, TabValues } from './types'; +import { useOutletContext } from 'react-router-dom'; +import { Palette } from 'styles/Theme'; +import { Unless, When } from 'react-if'; +import { TabContext, TabPanel } from '@mui/lab'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faX } from '@fortawesome/pro-regular-svg-icons'; +import { EditorState } from 'draft-js'; + +const handleDuplicateTabNames = (newTabs: TabValues[], newTabName: string) => { + // Will add a sequencial number suffix for up to 10 numbers if there is a duplicate, then add (copy) if none of those are available. + for (let i = 2; i < 12; i++) { + if (!newTabs.find((tab) => tab.heading === `${newTabName} (${i})`)) { + return `${newTabName} (${i})`; + } + } + return `${newTabName} (copy)`; +}; + +const AuthoringDetails = () => { + const { + setValue, + watch, // Optional form control prop + control, // Optional form control prop + engagement, // Optional form control prop + contentTabsEnabled, + tabs, + setTabs, + setSingleContentValues, + setContentTabsEnabled, + singleContentValues, + defaultTabValues, + }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + const [currentTab, setCurrentTab] = useState(tabs[0]); + + const tabsStyles = { + borderBottom: `2px solid ${colors.surface.gray[60]}`, + overflow: 'hidden', + '& .MuiTabs-flexContainer': { + justifyContent: 'flex-start', + width: 'max-content', + }, + }; + const tabStyles = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '48px', + padding: '4px 18px 2px 18px', + fontSize: '14px', + borderRadius: '0px 16px 0px 0px', + border: `1px solid ${colors.surface.gray[60]}`, + borderBottom: 'none', + boxShadow: + '0px 2px 5px 0px rgba(0, 0, 0, 0.12), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.20)', + backgroundColor: 'gray.10', + color: 'text.secondary', + fontWeight: 'normal', + '&.Mui-selected': { + backgroundColor: 'primary.main', + borderColor: 'primary.main', + color: 'white', + fontWeight: 'bold', + }, + outlineOffset: '-4px', + '&:focus-visible': { + outline: `2px solid`, + outlineColor: '#12508F', + border: '4px solid', + borderColor: '#12508F', + padding: '0px 20px 0px 14px', + }, + }; + + const handleCloseTab = (e: React.MouseEvent, tab: TabValues) => { + e.stopPropagation(); + const index = tabs.findIndex((t) => t.heading === tab.heading); + if (-1 < index) { + const newTabs = [...tabs]; + newTabs.splice(index, 1); + if (1 === newTabs.length) { + // If we're switching back to single content mode + 'Tab 1' !== tabs[0].heading + ? setSingleContentValues(tabs[0]) + : setSingleContentValues({ ...tabs[0], heading: '' }); // If the current Section Heading is "Tab 1" then change it to a blank value. + setTabs([tabs[0]]); + setContentTabsEnabled('false'); + } else { + setTabs(newTabs); + tab === currentTab && setCurrentTab(newTabs[index - 1]); // Switch tabs if you're closing the current one + } + } + }; + + const handleAddTab = () => { + const newTabs = [...tabs]; + const newTabName = 'Tab ' + (newTabs.length + 1); + newTabs.find((tab) => newTabName === tab.heading) + ? newTabs.push({ ...defaultTabValues, heading: handleDuplicateTabNames(newTabs, newTabName) }) + : newTabs.push({ ...defaultTabValues, heading: newTabName }); // Don't create duplicate entries + setTabs(newTabs); + setCurrentTab(newTabs[newTabs.length - 1]); + }; + + return ( + + + + tab.heading === currentTab.heading) + ? tabs[tabs.findIndex((t) => t.heading === currentTab.heading)].heading + : tabs[tabs.length - 1].heading + } + > + tab.heading === currentTab.heading) + ? tabs[tabs.findIndex((t) => t.heading === currentTab.heading)].heading + : tabs[tabs.length - 1].heading + } + onChange={(event: SyntheticEvent, value: string) => + tabs.find((tab) => tab.heading === value) && + setCurrentTab(tabs[tabs.findIndex((tab) => tab.heading === value)]) + } + > + {tabs.map((tab, key) => { + return ( + + {tab.heading} + + handleCloseTab(e, tab)} + > + + + + + } + key={key} + value={tab.heading} + disableFocusRipple + /> + ); + })} + + + {tabs.map((tab, key) => { + return ( + + + + ); + })} + + + + + + + + ); +}; + +export default AuthoringDetails; + +const DetailsTab = ({ + setValue, + setTabs, + setCurrentTab, + setSingleContentValues, + tabs, + tabIndex, + singleContentValues, + defaultTabValues, +}: DetailsTabProps) => { + // Define the styles + const metBigLabelStyles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + lineHeight: 1.167, + color: '#292929', + fontWeight: '700', + }; + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const formDescriptionTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleSectionHeadingChange = (value: string) => { + const newHeading = value; + if (2 > tabs.length && 0 === tabIndex) { + // If there are no tabs + setSingleContentValues({ ...singleContentValues, heading: newHeading }); + setTabs([{ ...defaultTabValues, heading: newHeading }]); + } else { + // If there are tabs + const newTabs = [...tabs]; + newTabs[tabIndex].heading = newTabs.find((tab) => tab.heading === newHeading) + ? handleDuplicateTabNames(newTabs, newHeading) + : newHeading; // If the new name is the same as an existing one, rename it + setSingleContentValues(newTabs[0]); + setTabs([...newTabs]); + setCurrentTab(newTabs[tabIndex]); + } + }; + + const handleWidgetChange = (event: SelectChangeEvent) => { + const newWidget = event.target.value; + const newTabs = [...tabs]; + newTabs[tabIndex].widget = newWidget; + setTabs(newTabs); + }; + + const handleRemoveWidget = () => { + if ('' === tabs[tabIndex].widget) { + return; + } else { + const newTabs = [...tabs]; + newTabs[tabIndex].widget = ''; + setTabs(newTabs); + } + }; + + const handleBodyTextChange = (newEditorState: EditorState) => { + const plainText = newEditorState.getCurrentContent().getPlainText(); + const newTabs = [...tabs]; + newTabs[tabIndex].bodyCopyEditorState = newEditorState; + newTabs[tabIndex].bodyCopyPlainText = plainText; + setTabs(newTabs); + }; + + return ( + + + Primary Content (Required) + + Primary content will display on the left two thirds of the page on large screens and full width on + small screens. (If you add optional supporting content in the section below, on small screens, your + primary content will display first (on top) followed by your supporting content (underneath). + + + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. On large screens this + content will be displayed to the right of your primary content. On small screens this content will + be displayed below your primary content. + + + + + + + + + + {tabs[tabIndex].widget} {tabs[tabIndex].widget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx new file mode 100644 index 000000000..63ccc6a32 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringFeedback.tsx @@ -0,0 +1,288 @@ +import { Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import { EyebrowText as FormDescriptionText } from 'components/common/Typography'; +import { colors, MetLabel, MetHeader3, MetLabel as MetBigLabel } from 'components/common'; +import { Button, TextField } from 'components/common/Input'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { EditorState } from 'draft-js'; +import React, { useState } from 'react'; +import { Palette } from 'styles/Theme'; + +const AuthoringFeedback = () => { + const [sectionHeading, setSectionHeading] = useState(''); + const [bodyText, setBodyText] = useState(''); + const [editorState, setEditorState] = useState(); + const [surveyButtonText, setSurveyButtonText] = useState(''); + const [thirdPartyCtaText, setThirdPartyCtaText] = useState(''); + const [thirdPartyCtaLink, setThirdPartyCtaLink] = useState(''); + const [currentWidget, setCurrentWidget] = useState(''); + + // Define the styles + const metBigLabelStyles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + lineHeight: 1.167, + color: '#292929', + fontWeight: '700', + }; + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const formDescriptionTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleEditorChange = (newEditorState: EditorState) => { + const plainText = newEditorState.getCurrentContent().getPlainText(); + setEditorState(newEditorState); + setBodyText(plainText); + }; + + const handleWidgetChange = (event: SelectChangeEvent) => { + setCurrentWidget(event.target.value); + }; + + const handleRemoveWidget = () => { + if ('' === currentWidget) { + return; + } else { + setCurrentWidget(''); + } + }; + + return ( + + + Primary Content (Required) + + This section of content should provide a brief overview of what your engagement is about and what + you would like your audience to do. + + + + + + + + + + + + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. On large screens this + content will be displayed to the right of your primary content. On small screens this content will + be displayed below your primary content. + + + + + + + + + + {currentWidget} {currentWidget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; + +export default AuthoringFeedback; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx new file mode 100644 index 000000000..12e713506 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringMore.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AuthoringMore = () => { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringMore; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx new file mode 100644 index 000000000..f49485544 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringNavElements.tsx @@ -0,0 +1,78 @@ +import { USER_ROLES } from 'services/userService/constants'; +import { TenantState } from 'reduxSlices/tenantSlice'; + +export interface AuthoringRoute { + name: string; + path: string; + base: string; + authenticated: boolean; + allowedRoles: string[]; + required?: boolean; +} + +export const getAuthoringRoutes = (engagementId: number, tenant: TenantState): AuthoringRoute[] => [ + { + name: 'Engagement Home', + path: `/${tenant.id}/engagements/${engagementId}/details/authoring`, + base: `/${tenant.id}/engagements`, + authenticated: false, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Hero Banner', + path: `/engagements/${engagementId}/details/authoring/banner`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Summary', + path: `/engagements/${engagementId}/details/authoring/summary`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Details', + path: `/engagements/${engagementId}/details/authoring/details`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'Provide Feedback', + path: `/engagements/${engagementId}/details/authoring/feedback`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: true, + }, + { + name: 'View Results', + path: `/engagements/${engagementId}/details/authoring/results`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, + { + name: 'Subscribe', + path: `/engagements/${engagementId}/details/authoring/subscribe`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, + { + name: 'More Engagements', + path: `/engagements/${engagementId}/details/authoring/more`, + base: `/engagements`, + authenticated: true, + allowedRoles: [USER_ROLES.EDIT_ENGAGEMENT], + required: false, + }, +]; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx new file mode 100644 index 000000000..6a7d282e7 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringResults.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AuthoringResults = () => { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringResults; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx new file mode 100644 index 000000000..76fc7fc85 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSideNav.tsx @@ -0,0 +1,281 @@ +import React from 'react'; +import { + ListItemButton, + List, + ListItem, + Box, + Drawer, + Toolbar, + SwipeableDrawer, + Grid, + Avatar, + ThemeProvider, +} from '@mui/material'; +import { getAuthoringRoutes as getRoutes, AuthoringRoute as Route } from './AuthoringNavElements'; +import { DarkTheme, Palette, colors, ZIndex } from 'styles/Theme'; +import { AuthoringNavProps, DrawerBoxProps } from './types'; +import { When } from 'react-if'; +import { useAppSelector } from 'hooks'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPencil } from '@fortawesome/pro-light-svg-icons/faPencil'; +import { Link } from 'components/common/Navigation'; +import { BodyText } from 'components/common/Typography'; +import { USER_ROLES } from 'services/userService/constants'; +import UserService from 'services/userService'; +import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; +import { faArrowLeftLong } from '@fortawesome/pro-light-svg-icons'; +import { faCheck } from '@fortawesome/pro-regular-svg-icons'; +import { StatusCircle } from '../../view/AuthoringTab'; + +export const routeItemStyle = { + padding: 0, + backgroundColor: Palette.background.default, + '&:hover, &:focus': { + filter: 'brightness(96%)', + }, + '&:active': { + filter: 'brightness(92%)', + }, + borderRadius: '8px', +}; + +const DrawerBox = ({ isMediumScreenOrLarger, setOpen, engagementId }: DrawerBoxProps) => { + const permissions = useAppSelector((state) => state.user.roles); + const tenant = useAppSelector((state) => state.tenant); + + const currentRoutePath = getRoutes(Number(engagementId), tenant) + .map((route) => route.path) + .filter((route) => location.pathname.includes(route)) + .reduce((prev, curr) => (prev.length > curr.length ? prev : curr)); + + const allowedRoutes = getRoutes(Number(engagementId), tenant).filter((route) => { + return !route.authenticated || route.allowedRoles.some((role) => permissions.includes(role)); + }); + + const renderListItem = (route: Route, itemType: string, key: number, engagementId: number) => { + const navLabelTextStyles: React.CSSProperties = { + display: 'flex', + textTransform: 'uppercase', + fontWeight: 'bold', + fontSize: '0.95rem', + color: Palette.text.primary, + marginTop: '1.4rem', + marginBottom: '1rem', + }; + return ( + <> + + + {'Hero Banner' === route.name ? 'Required' : 'Optional'} Sections + + + + { + setOpen(false); + }} + > + + + + + + + {route.name} + + + + + + + + + + + ); + }; + + return ( + + + {/* Engagement Home link */} + + + {getRoutes(Number(engagementId), tenant)[0].name} + +
+ {/* All other menu items */} + {allowedRoutes.map( + (route, index) => + 0 !== index && + renderListItem( + route, + currentRoutePath === route.path ? 'selected' : 'other', + index, + Number(engagementId), + ), + )} +
+
+ ); +}; + +const AuthoringSideNav = ({ open, setOpen, isMediumScreen, engagementId }: AuthoringNavProps) => { + const currentUser = useAppSelector((state) => state.user.userDetail.user); + if (isMediumScreen) + return ( + + + + + ); + return ( + theme.zIndex.drawer + 3, // render above feedback button + }} + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + anchor={'top'} + open={open} + disableEnforceFocus + disablePortal + > + + + + + + + {currentUser?.first_name[0]} + {currentUser?.last_name[0]} + + + + + Hello {currentUser?.first_name} + + + {currentUser?.roles.includes(USER_ROLES.SUPER_ADMIN) + ? 'Super Admin' + : currentUser?.main_role ?? 'User'} + + + + + Logout + + + + + + + + ); +}; + +export default AuthoringSideNav; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSubscribe.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSubscribe.tsx new file mode 100644 index 000000000..eb9e27be4 --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSubscribe.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const AuthoringSubscribe = () => { + return ( + <> +

Hello world

+ + + ); +}; + +export default AuthoringSubscribe; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx new file mode 100644 index 000000000..6f4ce5f3c --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringSummary.tsx @@ -0,0 +1,216 @@ +import { Grid, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import React, { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { TextField } from 'components/common/Input'; +import { AuthoringTemplateOutletContext } from './types'; +import { colors, Palette } from 'styles/Theme'; +import { EyebrowText as FormDescriptionText } from 'components/common/Typography'; +import { MetLabel, MetHeader3, MetLabel as MetBigLabel } from 'components/common'; +import { Button } from 'components/common/Input'; +import { RichTextArea } from 'components/common/Input/RichTextArea'; +import { EditorState } from 'draft-js'; + +const AuthoringBanner = () => { + const { + setValue, // Optional form control prop + watch, // Optional form control prop + control, // Optional form control prop + engagement, // Engagement for populating values + }: AuthoringTemplateOutletContext = useOutletContext(); // Access the form functions and values from the authoring template + + const [editorState, setEditorState] = useState(); + const [bodyText, setBodyText] = useState(''); + const [currentWidget, setCurrentWidget] = useState(''); + + //Define the styles + const metBigLabelStyles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + lineHeight: 1.167, + color: '#292929', + fontWeight: '700', + }; + const metHeader3Styles = { + fontSize: '1.05rem', + marginBottom: '0.7rem', + }; + const formDescriptionTextStyles = { + fontSize: '0.9rem', + marginBottom: '1.5rem', + }; + const metLabelStyles = { + fontSize: '0.95rem', + }; + const formItemContainerStyles = { + padding: '2rem 1.4rem !important', + margin: '1rem 0', + borderRadius: '16px', + }; + const conditionalSelectStyles = { + width: '100%', + backgroundColor: colors.surface.white, + borderRadius: '8px', + boxShadow: '0 0 0 1px #7A7876 inset', + lineHeight: '1.4375em', + height: '48px', + marginTop: '8px', + padding: '0', + }; + const widgetPreviewStyles = { + margin: '2rem 4rem 4rem', + display: 'flex', + minHeight: '18rem', + border: '2px dashed rgb(122, 120, 118)', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '16px', + }; + const buttonStyles = { + height: '2.6rem', + borderRadius: '8px', + border: 'none', + padding: '0 1rem', + minWidth: '8.125rem', + fontSize: '0.9rem', + }; + + const toolbar = { + options: ['inline', 'list', 'link', 'blockType', 'history'], + inline: { + options: ['bold', 'italic', 'underline', 'superscript', 'subscript'], + }, + blockType: { options: ['Normal', 'H2', 'H3', 'Blockquote'] }, + list: { options: ['unordered', 'ordered'] }, + }; + + const handleWidgetSelectChange = (event: SelectChangeEvent) => { + const newWidgetValue = event.target.value; + setCurrentWidget(newWidgetValue); + }; + + const handleEditorChange = (newEditorState: EditorState) => { + console.log(newEditorState); + const plainText = newEditorState.getCurrentContent().getPlainText(); + setEditorState(newEditorState); + setBodyText(plainText); + }; + + return ( + + + + + + + + + + + Supporting Content (Optional) + + You may use a widget to add supporting content to your primary content. + + + + + + + + + + {currentWidget} {currentWidget && 'Widget'} + + + {/* todo: show a preview of the widget here */} + Widget Preview + + + + + + ); +}; + +export default AuthoringBanner; diff --git a/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx new file mode 100644 index 000000000..5edd07f8f --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/AuthoringTemplate.tsx @@ -0,0 +1,202 @@ +import React, { Suspense, useMemo, useState } from 'react'; +import { useOutletContext, Form, useParams, useRouteLoaderData, Await, Outlet } from 'react-router-dom'; +import AuthoringBottomNav from './AuthoringBottomNav'; +import { EngagementUpdateData } from './AuthoringContext'; +import { useFormContext } from 'react-hook-form'; +import UnsavedWorkConfirmation from 'components/common/Navigation/UnsavedWorkConfirmation'; +import { AuthoringContextType, StatusLabelProps } from './types'; +import { Engagement } from 'models/engagement'; +import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; +import { ResponsiveContainer } from 'components/common/Layout'; +import { EngagementStatus } from 'constants/engagementStatus'; +import { EyebrowText, Header2 } from 'components/common/Typography'; +import { useAppSelector } from 'hooks'; +import { getTenantLanguages } from 'services/languageService'; +import { Language } from 'models/language'; +import { getAuthoringRoutes } from './AuthoringNavElements'; +import { FormControlLabel, Grid, Radio, RadioGroup } from '@mui/material'; +import { SystemMessage } from 'components/common/Layout/SystemMessage'; +import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; + +export const StatusLabel = ({ text, completed }: StatusLabelProps) => { + const statusLabelStyle = { + background: true === completed ? '#42814A' : '#CE3E39', + padding: '0.2rem 0.75rem', + color: '#ffffff', + borderRadius: '3px', + fontSize: '0.8rem', + }; + return {text}; +}; + +export const getLanguageValue = (currentLanguage: string, languages: Language[]) => { + return languages.find((language) => language.code === currentLanguage)?.name; +}; + +const AuthoringTemplate = () => { + const { onSubmit }: AuthoringContextType = useOutletContext(); + const { engagementId } = useParams() as { engagementId: string }; // We need the engagement ID quickly, so let's grab it from useParams + const { engagement } = useRouteLoaderData('single-engagement') as { engagement: Engagement }; + const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id)); + const [contentTabsEnabled, setContentTabsEnabled] = useState('false'); // todo: replace default value with stored value in engagement. + const defaultTabValues = { + heading: 'Tab 1', + bodyCopyPlainText: '', + bodyCopyEditorState: getEditorStateFromRaw(''), + widget: '', + }; + const [tabs, setTabs] = useState([defaultTabValues]); + const [singleContentValues, setSingleContentValues] = useState({ ...defaultTabValues, heading: '' }); + const tenant = useAppSelector((state) => state.tenant); + const languages = useMemo(() => getTenantLanguages(tenant.id), [tenant.id]); // todo: Using tenant language list until language data is integrated with the engagement. + const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant); + const location = window.location.href; + const locationArray = location.split('/'); + const slug = locationArray[locationArray.length - 1]; + const pageTitle = authoringRoutes.find((route) => { + const pathArray = route.path.split('/'); + const pathSlug = pathArray[pathArray.length - 1]; + return pathSlug === slug; + })?.name; + const { + handleSubmit, + setValue, + watch, + control, + formState: { isDirty, isValid, isSubmitting }, + } = useFormContext(); + + // Set hidden values for form data + setValue('id', Number(engagementId)); + + const eyebrowTextStyles = { + fontSize: '0.9rem', + marginBottom: '1rem', + }; + + // If switching from single to multiple, add "tab 2". If switching from multiple to single, remove all values but index 0. + const handleTabsRadio = (event: React.ChangeEvent) => { + const newTabsRadioValue = event.target.value; + if ('true' === newTabsRadioValue) { + setTabs([ + { ...tabs[0], heading: singleContentValues.heading ? singleContentValues.heading : 'Tab 1' }, + { ...defaultTabValues, heading: 'Tab 2' }, + ]); + setContentTabsEnabled('true'); + } else { + setSingleContentValues({ ...tabs[0], heading: 'Tab 1' !== tabs[0].heading ? tabs[0].heading : '' }); + setTabs([tabs[0]]); + setContentTabsEnabled('false'); + } + }; + + return ( + + + + + {(engagement: Engagement) => ( +
+ + {/* todo: For the section status label when it's ready */} + {/* */} +
+ )} +
+
+

{pageTitle}

+ + Under construction - the settings in this section have no effect. + + {'details' === slug && ( + + + Content Configuration + + + In the Details Section of your engagement, you have the option to display your content in a + normal, static page section view (No Tabs), or for lengthy content, use Tabs. You may wish to + use tabs if your content is quite lengthy so you can organize it into smaller, more digestible + chunks and reduce the length of your engagement page. + + + + } + label="No Tabs" + /> + + + } + label="Tabs (2 Minimum)" + /> + + + + )} + + + + {(languages: Language[]) => getLanguageValue(currentLanguage, languages) + ' Content'} + + + + +
+ + + {(engagement: Engagement) => ( + + )} + + + + + + + {(languages: Language[]) => ( + + )} + + + +
+ ); +}; + +export default AuthoringTemplate; diff --git a/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx new file mode 100644 index 000000000..8a5092a1f --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/engagementAuthoringUpdateAction.tsx @@ -0,0 +1,95 @@ +import { ActionFunction, redirect } from 'react-router-dom'; +import { patchEngagement } from 'services/engagementService'; +import { patchEngagementContent } from 'services/engagementContentService'; +import { patchEngagementMetadata } from 'services/engagementMetadataService'; +import { patchEngagementSettings } from 'services/engagementSettingService'; +import { patchEngagementSlug } from 'services/engagementSlugService'; +import { openNotification } from 'services/notificationService/notificationSlice'; + +export const engagementAuthoringUpdateAction: ActionFunction = async ({ request }) => { + const formData = (await request.formData()) as FormData; + const errors = []; + const requestType = formData.get('request_type') as string; + const engagement = await patchEngagement({ + id: Number(formData.get('id')) as unknown as number, + name: (formData.get('name') as string) || undefined, + start_date: (formData.get('start_date') as string) || undefined, + status_id: (Number(formData.get('status_id')) as unknown as number) || undefined, + end_date: (formData.get('end_date') as string) || undefined, + description: (formData.get('description') as string) || undefined, + rich_description: (formData.get('rich_description') as string) || undefined, + banner_filename: (formData.get('banner_filename') as string) || undefined, + status_block: (formData.get('status_block') as unknown as unknown[]) || undefined, + }); + + // Update engagement content. + if ( + (formData.get('title') || formData.get('text_content' || formData.get('json_content'))) && + '0' !== formData.get('content_id') + ) { + try { + await patchEngagementContent(engagement.id, Number(formData.get('content_id')) as unknown as number, { + title: (formData.get('title') as string) || undefined, + text_content: (formData.get('text_content') as string) || undefined, + json_content: (formData.get('json_content') as string) || undefined, + }); + } catch (e) { + console.error('Error updating engagement', e); + errors.push(e); + } + } + + // Update engagement metadata if necessary. + if (formData.get('metadata_value') && formData.get('taxon_id')) { + try { + await patchEngagementMetadata({ + value: formData.get('metadata_value') as string, + taxon_id: Number(formData.get('taxon_id')) as unknown as number, + engagement_id: engagement.id, + }); + } catch (e) { + console.error('Error updating engagement metadata', e); + errors.push(e); + } + } + + // Update engagement settings if necessary. + if (formData.get('send_report')) { + try { + await patchEngagementSettings({ + engagement_id: engagement.id, + send_report: 'true' === formData.get('send_report') ? true : false, + }); + } catch (e) { + console.error('Error updating engagement settings', e); + errors.push(e); + } + } + + // Update engagement slug if necessary. + if (formData.get('slug')) { + try { + await patchEngagementSlug({ + engagement_id: engagement.id, + slug: formData.get('slug') as string, + }); + } catch (e) { + console.error('Error updating engagement slug', e); + errors.push(e); + } + } + + if (0 === errors.length && 'preview' === requestType) { + return redirect(`/engagements/${engagement.id}/old-view`); + } else if (0 === errors.length && 'update' === requestType) { + openNotification({ + severity: 'success', + text: 'Engagement saved successfully.', + }); + } else { + openNotification({ + severity: 'error', + text: 'preview' === requestType ? 'Unable to preview engagement' : 'Unable to save engagement', + }); + } +}; diff --git a/met-web/src/components/engagement/admin/create/authoring/types.ts b/met-web/src/components/engagement/admin/create/authoring/types.ts new file mode 100644 index 000000000..810fc667b --- /dev/null +++ b/met-web/src/components/engagement/admin/create/authoring/types.ts @@ -0,0 +1,78 @@ +import { Control, SubmitHandler, UseFormSetValue, UseFormWatch } from 'react-hook-form'; +import { EngagementUpdateData } from './AuthoringContext'; +import { Dispatch, SetStateAction } from 'react'; +import { Language } from 'models/language'; +import { Engagement } from 'models/engagement'; +import { EditorState } from 'draft-js'; + +export interface AuthoringNavProps { + open: boolean; + isMediumScreen: boolean; + setOpen: (open: boolean) => void; + engagementId: string; +} + +export interface DrawerBoxProps { + isMediumScreenOrLarger: boolean; + setOpen: (open: boolean) => void; + engagementId: string; +} + +export interface AuthoringContextType { + onSubmit: SubmitHandler; +} + +export interface LanguageSelectorProps { + currentLanguage: string; + setCurrentLanguage: Dispatch>; + languages: Language[]; + isDirty: boolean; + isSubmitting: boolean; +} + +export interface AuthoringBottomNavProps { + isDirty: boolean; + isValid: boolean; + isSubmitting: boolean; + currentLanguage: string; + setCurrentLanguage: Dispatch>; + languages: Language[]; + pageTitle: string; +} + +export interface StatusLabelProps { + text: string; + completed: boolean; +} + +export interface AuthoringTemplateOutletContext { + setValue: UseFormSetValue; + watch: UseFormWatch; + control: Control; + engagement: Engagement; + contentTabsEnabled: string; + tabs: TabValues[]; + setTabs: Dispatch>; + singleContentValues: TabValues; + setSingleContentValues: Dispatch>; + setContentTabsEnabled: Dispatch>; + defaultTabValues: TabValues; +} + +export interface DetailsTabProps { + setValue: UseFormSetValue; + setTabs: Dispatch>; + setCurrentTab: Dispatch>; + setSingleContentValues: Dispatch>; + tabs: TabValues[]; + tabIndex: number; + singleContentValues: TabValues; + defaultTabValues: TabValues; +} + +export interface TabValues { + heading: string; + bodyCopyPlainText: string; + bodyCopyEditorState: EditorState; + widget: string; +} diff --git a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx index a4c3710ce..c25bc1026 100644 --- a/met-web/src/components/engagement/admin/view/AuthoringTab.tsx +++ b/met-web/src/components/engagement/admin/view/AuthoringTab.tsx @@ -10,15 +10,18 @@ import { Unless, When } from 'react-if'; import { Grid } from '@mui/material'; import { colors } from 'styles/Theme'; import { Link } from 'components/common/Navigation'; +import { getDefaultAuthoringTabValues } from './AuthoringTabElements'; -const StatusCircle = (props: StatusCircleProps) => { +export const StatusCircle = (props: StatusCircleProps) => { const statusCircleStyles = { width: '6px', height: '6px', borderRadius: '50%', backgroundColor: props.required ? colors.notification.danger.icon : colors.surface.gray[70], marginLeft: '0.3rem', - marginTop: '-1rem', + display: 'inline-block', + bottom: '0.5rem', + position: 'relative' as const, }; return ; }; @@ -67,38 +70,9 @@ const AuthoringButton = (props: AuthoringButtonProps) => { }; export const AuthoringTab = () => { - // Set default values - const mandatorySectionTitles = ['Hero Banner', 'Summary', 'Details', 'Provide Feedback']; - const optionalSectionTitles = ['View Results', 'Subscribe', 'More Engagements']; - const feedbackTitles = ['Survey', '3rd Party Feedback Method Link']; - const defaultAuthoringValue: AuthoringValue = { - id: 0, - title: '', - link: '#', - required: false, - completed: false, - }; - const getAuthoringValues = ( - defaultValues: AuthoringValue, - titles: string[], - required: boolean, - idOffset = 0, - ): AuthoringValue[] => { - return titles.map((title, index) => ({ - ...defaultValues, - title: title, - required: required, - id: index + idOffset, - })); - }; - const mandatorySectionValues = getAuthoringValues(defaultAuthoringValue, mandatorySectionTitles, true); - const optionalSectionValues = getAuthoringValues(defaultAuthoringValue, optionalSectionTitles, false, 100); - const defaultSectionValues = [...mandatorySectionValues, ...optionalSectionValues]; - const defaultFeedbackMethods = getAuthoringValues(defaultAuthoringValue, feedbackTitles, true, 1000); - // Set useStates. When data is imported, it will be set with setSectionValues and setFeedbackMethods. - const [sectionValues] = useState(defaultSectionValues); - const [feedbackMethods] = useState(defaultFeedbackMethods); + const [sectionValues] = useState(getDefaultAuthoringTabValues('sections')); + const [feedbackMethods] = useState(getDefaultAuthoringTabValues('feedback')); const [requiredSectionsCompleted, setRequiredSectionsCompleted] = useState(false); const [feedbackCompleted, setFeedbackCompleted] = useState(false); diff --git a/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx b/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx new file mode 100644 index 000000000..85e9f0b59 --- /dev/null +++ b/met-web/src/components/engagement/admin/view/AuthoringTabElements.tsx @@ -0,0 +1,76 @@ +import { AuthoringValue } from './types'; + +export const getDefaultAuthoringTabValues = (type: string): AuthoringValue[] => { + if ('sections' === type) { + // Return the default "section" items + return [ + { + id: 1, + title: 'Hero Banner', + link: `banner`, + required: true, + completed: false, + }, + { + id: 2, + title: 'Summary', + link: `summary`, + required: true, + completed: false, + }, + { + id: 3, + title: 'Details', + link: `details`, + required: true, + completed: false, + }, + { + id: 4, + title: 'Provide Feedback', + link: `feedback`, + required: true, + completed: false, + }, + { + id: 5, + title: 'View Results', + link: `results`, + required: false, + completed: false, + }, + { + id: 6, + title: 'Subscribe', + link: `subscribe`, + required: false, + completed: false, + }, + { + id: 7, + title: 'More Engagements', + link: `more`, + required: false, + completed: false, + }, + ]; + } else { + // Return the default "feedback" items + return [ + { + id: 101, + title: 'Survey', + link: `#`, + required: true, + completed: false, + }, + { + id: 102, + title: '3rd Party Feedback Method Link', + link: `#`, + required: true, + completed: false, + }, + ]; + } +}; diff --git a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx index d24a50906..8da2b2d6e 100644 --- a/met-web/src/components/engagement/admin/view/ConfigSummary.tsx +++ b/met-web/src/components/engagement/admin/view/ConfigSummary.tsx @@ -179,7 +179,7 @@ export const ConfigSummary = () => {
- diff --git a/met-web/src/components/engagement/admin/view/index.tsx b/met-web/src/components/engagement/admin/view/index.tsx index 6455e998f..91b7de65a 100644 --- a/met-web/src/components/engagement/admin/view/index.tsx +++ b/met-web/src/components/engagement/admin/view/index.tsx @@ -1,27 +1,27 @@ -import React, { Suspense, useState } from 'react'; -import { useRouteLoaderData, Await } from 'react-router-dom'; +import React, { Suspense } from 'react'; +import { useRouteLoaderData, Await, useMatches, UIMatch, Outlet } from 'react-router-dom'; import { Engagement } from 'models/engagement'; import { AutoBreadcrumbs } from 'components/common/Navigation/Breadcrumb'; import { EngagementStatus } from 'constants/engagementStatus'; import { Tab } from '@mui/material'; import { ResponsiveContainer } from 'components/common/Layout'; -import { ConfigSummary } from './ConfigSummary'; -import { AuthoringTab } from './AuthoringTab'; import { TabContext, TabList, TabPanel } from '@mui/lab'; import { EngagementLoaderData } from 'components/engagement/public/view'; +import { RouterLinkRenderer } from 'components/common/Navigation/Link'; export const AdminEngagementView = () => { const { engagement, teamMembers, slug } = useRouteLoaderData('single-engagement') as EngagementLoaderData; const EngagementViewTabs = { config: 'Configuration', - author: 'Authoring', + authoring: 'Authoring', activity: 'Activity', results: 'Results', publish: 'Publishing', }; - const [currentTab, setCurrentTab] = useState(EngagementViewTabs.config); + const matches = useMatches() as UIMatch[]; + const currentTab = matches[matches.length - 1].pathname.split('/').pop() ?? ''; return ( @@ -50,7 +50,6 @@ export const AdminEngagementView = () => { setCurrentTab(newValue)} aria-label="Admin Engagement View Tabs" TabIndicatorProps={{ sx: { display: 'none' } }} sx={{ @@ -63,9 +62,11 @@ export const AdminEngagementView = () => { {Object.entries(EngagementViewTabs).map(([key, value]) => ( { ))} - + - + - - - - - ); diff --git a/met-web/src/components/engagement/listing/index.tsx b/met-web/src/components/engagement/listing/index.tsx index ea14b4fed..b43b3755c 100644 --- a/met-web/src/components/engagement/listing/index.tsx +++ b/met-web/src/components/engagement/listing/index.tsx @@ -131,7 +131,7 @@ const EngagementListing = () => { label: 'Engagement Name', allowSort: true, renderCell: (row: Engagement) => ( - + {row.name} ), diff --git a/met-web/src/components/feedback/FeedbackModal/index.tsx b/met-web/src/components/feedback/FeedbackModal/index.tsx index 19e497d19..8647f83dd 100644 --- a/met-web/src/components/feedback/FeedbackModal/index.tsx +++ b/met-web/src/components/feedback/FeedbackModal/index.tsx @@ -79,7 +79,7 @@ export const FeedbackModal = () => { const isCommentNotProvided = comment_type !== CommentTypeEnum.None && !comment; const theme = useTheme(); - const bottomSpacing = theme.spacing(40); + const bottomSpacing = theme.spacing(52); const rightSpacing = theme.spacing(0); return ( diff --git a/met-web/src/components/imageUpload/index.tsx b/met-web/src/components/imageUpload/index.tsx index 83abb6a2c..6a1f53cf0 100644 --- a/met-web/src/components/imageUpload/index.tsx +++ b/met-web/src/components/imageUpload/index.tsx @@ -42,7 +42,7 @@ export const ImageUpload = ({ > - + {helpText} diff --git a/met-web/src/components/layout/Header/InternalHeader.tsx b/met-web/src/components/layout/Header/InternalHeader.tsx index 72b2f79ab..a3f1a28ad 100644 --- a/met-web/src/components/layout/Header/InternalHeader.tsx +++ b/met-web/src/components/layout/Header/InternalHeader.tsx @@ -23,15 +23,17 @@ import { Link } from 'components/common/Navigation'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBars, faChevronDown, faClose, faSignOut } from '@fortawesome/pro-regular-svg-icons'; import UserService from 'services/userService'; -import { Await, useAsyncValue, useRouteLoaderData } from 'react-router-dom'; +import { Await, useAsyncValue, useRouteLoaderData, useParams } from 'react-router-dom'; import { Tenant } from 'models/tenant'; -import { When, Unless } from 'react-if'; +import { When, Unless, If, Else, Then } from 'react-if'; import { Button } from 'components/common/Input'; import DropdownMenu, { dropdownMenuStyles } from 'components/common/Navigation/DropdownMenu'; import { elevations } from 'components/common'; import TrapFocus from '@mui/base/TrapFocus'; import SideNav from '../SideNav/SideNav'; import { USER_ROLES } from 'services/userService/constants'; +import AuthoringSideNav from '../../engagement/admin/create/authoring/AuthoringSideNav'; +import { getAuthoringRoutes } from '../../engagement/admin/create/authoring/AuthoringNavElements'; const InternalHeader = () => { const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md')); @@ -58,6 +60,19 @@ const InternalHeader = () => { } }, [isMediumScreenOrLarger, sideNavOpen]); + // Get the authoring nav elements and current route so we can check their last two slugs against the current route's last two slugs. + // This will be used to determine which sidenav menu is displayed. + const pathname = window.location.href; + const { engagementId } = useParams() as { engagementId: string }; + const currentAuthoringSlug = pathname.split('/').slice(-2).join('/'); + const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant).map((route) => { + // skip the "Engagement Home" link + if ('Engagement Home' !== route.name) { + const pathArray = route.path.split('/'); + return pathArray.slice(-2).join('/'); + } + }); + const { myTenants } = useRouteLoaderData('authenticated-root') as { myTenants: Tenant[] }; const sidePadding = { xs: '0 1em', md: '0 1.5em 0 2em', lg: '0 3em 0 2em' }; @@ -195,12 +210,27 @@ const InternalHeader = () => { - + + + <> + + + + + + + diff --git a/met-web/src/routes/AuthenticatedRoutes.tsx b/met-web/src/routes/AuthenticatedRoutes.tsx index fcf94f828..88e68da7e 100644 --- a/met-web/src/routes/AuthenticatedRoutes.tsx +++ b/met-web/src/routes/AuthenticatedRoutes.tsx @@ -40,6 +40,18 @@ import EngagementCreationWizard from 'components/engagement/admin/config/wizard/ import engagementCreateAction from 'components/engagement/admin/config/EngagementCreateAction'; import EngagementConfigurationWizard from 'components/engagement/admin/config/wizard/ConfigWizard'; import engagementUpdateAction from 'components/engagement/admin/config/EngagementUpdateAction'; +import { ConfigSummary as ConfigTab } from 'components/engagement/admin/view/ConfigSummary'; +import { AuthoringTab } from 'components/engagement/admin/view/AuthoringTab'; +import AuthoringBanner from 'components/engagement/admin/create/authoring/AuthoringBanner'; +import { engagementAuthoringUpdateAction } from 'components/engagement/admin/create/authoring/engagementAuthoringUpdateAction'; +import { AuthoringContext } from 'components/engagement/admin/create/authoring/AuthoringContext'; +import AuthoringTemplate from 'components/engagement/admin/create/authoring/AuthoringTemplate'; +import AuthoringSummary from 'components/engagement/admin/create/authoring/AuthoringSummary'; +import AuthoringDetails from 'components/engagement/admin/create/authoring/AuthoringDetails'; +import AuthoringFeedback from 'components/engagement/admin/create/authoring/AuthoringFeedback'; +import AuthoringResults from 'components/engagement/admin/create/authoring/AuthoringResults'; +import AuthoringSubscribe from 'components/engagement/admin/create/authoring/AuthoringSubscribe'; +import AuthoringMore from 'components/engagement/admin/create/authoring/AuthoringMore'; const AuthenticatedRoutes = () => { return ( @@ -92,29 +104,119 @@ const AuthenticatedRoutes = () => { crumb: async (data: { engagement: Promise }) => { return data.engagement.then((engagement) => { return { - link: `/engagements/${engagement.id}/view`, + link: `/engagements/${engagement.id}/old-view`, name: engagement.name, }; }); }, }} > - } /> }> } /> + + } /> + } /> + + } /> + {/* Wraps the tabs with the engagement title and TabContext */} + }> + } /> + }> + } /> + ({ name: 'Authoring' }) }} + element={} + > + }> + } action={engagementAuthoringUpdateAction}> + } + handle={{ + crumb: () => ({ + link: `banner`, + name: 'Hero Banner', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `summary`, + name: 'Summary', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `details`, + name: 'Details', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `feedback`, + name: 'Provide Feedback', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `results`, + name: 'View Results', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `subscribe`, + name: 'Subscribe', + }), + }} + /> + } + handle={{ + crumb: () => ({ + link: `more`, + name: 'More Engagements', + }), + }} + /> + + + + } /> + } action={engagementUpdateAction} handle={{ - crumb: () => ({ - name: 'Configure', - }), + crumb: () => ({ name: 'Configure' }), }} /> - } /> - } /> + }> + } /> + } /> } /> diff --git a/met-web/src/styles/Theme.ts b/met-web/src/styles/Theme.ts index edaf729dd..819bad08d 100644 --- a/met-web/src/styles/Theme.ts +++ b/met-web/src/styles/Theme.ts @@ -354,17 +354,6 @@ export const DarkTheme = createTheme({ }, }, }, - MuiSelect: { - defaultProps: { - MenuProps: { - sx: { - '&:hover': { - backgroundColor: DarkPalette.hover.light, - }, - }, - }, - }, - }, MuiButton: { styleOverrides: { root: {