diff --git a/src/App.jsx b/src/App.jsx index 60c75509f..00c8c5aac 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -86,7 +86,7 @@ const HowWeVoteHelps = React.lazy(() => import(/* webpackChunkName: 'HowWeVoteHe const Intro = React.lazy(() => import(/* webpackChunkName: 'Intro' */ './js/pages/Intro/Intro')); const IntroNetwork = React.lazy(() => import(/* webpackChunkName: 'IntroNetwork' */ './js/pages/Intro/IntroNetwork')); const Location = React.lazy(() => import(/* webpackChunkName: 'Location' */ './js/pages/Settings/Location')); -const ManageMyCandidates = React.lazy(() => import(/* webpackChunkName: 'ManageMyCandidates' */ './js/pages/More/ManageMyCandidates')); +const ManageMyCandidatesLanding = React.lazy(() => import(/* webpackChunkName: 'ManageMyCandidatesLanding' */ './js/pages/ManageMyCandidates/ManageMyCandidatesLanding')); const Measure = React.lazy(() => import(/* webpackChunkName: 'Measure' */ './js/pages/Ballot/Measure')); const News = React.lazy(() => import(/* webpackChunkName: 'News' */ './js/pages/Activity/News')); const Office = React.lazy(() => import(/* webpackChunkName: 'Office' */ './js/pages/Ballot/Office')); @@ -581,7 +581,8 @@ class App extends Component { - + + diff --git a/src/js/components/More/EditInvitationModal.jsx b/src/js/components/More/EditInvitationModal.jsx index a24e254ee..af3e189e0 100644 --- a/src/js/components/More/EditInvitationModal.jsx +++ b/src/js/components/More/EditInvitationModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import styled, { createGlobalStyle } from 'styled-components'; import { ContentCopy as CopyIcon } from '@mui/icons-material'; @@ -8,13 +8,22 @@ import ModalDisplayTemplateA from '../Widgets/ModalDisplayTemplateA'; const EditInvitationModal = ({ isOpen, onClose, - draftInvite, - setDraftInvite, - initialInvite, + invitationBody, onSave, notify, selectedPoliticianId, }) => { + const [draftInvite, setDraftInvite] = useState(invitationBody); + const [initialInvite, setInitialInvite] = useState(invitationBody); + + // Reset draft when invitationBody or showEdit changes + useEffect(() => { + if (isOpen) { + setDraftInvite(invitationBody); + setInitialInvite(invitationBody); + } + }, [invitationBody, isOpen]); + const handleCopyInviteBody = async () => { try { await navigator.clipboard.writeText( @@ -26,6 +35,10 @@ const EditInvitationModal = ({ } }; + const handleSave = () => { + onSave(draftInvite); + }; + if (!isOpen) return null; const dialogTitleJSX = ( @@ -59,7 +72,7 @@ const EditInvitationModal = ({ Close Save invitation @@ -88,9 +101,7 @@ const EditInvitationModal = ({ EditInvitationModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - draftInvite: PropTypes.string.isRequired, - setDraftInvite: PropTypes.func.isRequired, - initialInvite: PropTypes.string.isRequired, + invitationBody: PropTypes.string.isRequired, onSave: PropTypes.func.isRequired, notify: PropTypes.func.isRequired, selectedPoliticianId: PropTypes.string.isRequired, diff --git a/src/js/components/More/PasteListModal.jsx b/src/js/components/More/PasteListModal.jsx index 3803b6d13..df8a38db3 100644 --- a/src/js/components/More/PasteListModal.jsx +++ b/src/js/components/More/PasteListModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import styled, { createGlobalStyle } from 'styled-components'; import { WarningAmber as WarningIcon } from '@mui/icons-material'; @@ -7,16 +7,96 @@ import ModalDisplayTemplateA from '../Widgets/ModalDisplayTemplateA'; const escapeHTML = (s) => s.replace(/[&<>]/g, (element) => ({ '&': '&', '<': '<', '>': '>' }[element])); +const emailRE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i; + +function parsePastedList (text) { + const lines = text.split(/\r?\n/); + const rows = []; + const errors = []; + + lines.forEach((raw, idx) => { + const line = raw.trim(); + if (!line) return; + + const parts = line.split(',').map((s) => s.trim()).filter(Boolean); + const emailCount = (line.match(emailRE) || []).length; + + if (parts.length > 3) { + errors.push({ line: idx, reason: 'Too many commas' }); + return; + } + if (emailCount > 1) { + errors.push({ line: idx, reason: 'Two emails on one line (missing line break?)' }); + return; + } + // Must have at least "Name, Email" + if (parts.length < 2) { + errors.push({ line: idx, reason: 'Missing email (format: Name, email[, phone])' }); + return; + } + // Validate email + const emailPart = parts[1].replace(/[<>]/g, ''); + if (!emailRE.test(emailPart)) { + errors.push({ line: idx, reason: 'Invalid email' }); + return; + } + + rows.push({ + name: parts[0] || '', + email: emailPart || '', + phone: parts[2] || '', + }); + }); + + return { rows, errors }; +} + +function buildMirrorHTML (text, errors) { + const errSet = new Set(errors.map((e) => e.line)); + return text.split(/\r?\n/).map((line, i) => { + const isError = errSet.has(i) && line.trim().length > 0; + const safe = escapeHTML(line); + return isError ? `${safe}` : safe; + }).join('\n'); +} + export default function PasteListModal ({ isOpen, onClose, - pasteText, - onPasteTextChange, - mirrorHTML, - pasteErrors, onImport, - prospectiveCount, }) { + const [pasteText, setPasteText] = useState(''); + const [pasteErrors, setPasteErrors] = useState([]); + const [mirrorHTML, setMirrorHTML] = useState(''); + + const onPasteTextChange = (e) => { + const { value } = e.target; + setPasteText(value); + const { errors } = parsePastedList(value); + setPasteErrors(errors); + setMirrorHTML(buildMirrorHTML(value, errors)); + }; + + const prospectiveCount = useMemo(() => parsePastedList(pasteText).rows.length, [pasteText]); + + const handlePasteImport = () => { + const { rows, errors } = parsePastedList(pasteText); + if (errors.length) { + setPasteErrors(errors); + setMirrorHTML(buildMirrorHTML(pasteText, errors)); + return; // block import until fixed + } + onImport(rows); + }; + + const handleClose = () => { + // Reset state on close + setPasteText(''); + setPasteErrors([]); + setMirrorHTML(''); + onClose(); + }; + const dialogTitleJSX = ( Paste voters @@ -93,10 +173,10 @@ John Dough, jd@email.com, (213)-123-4567`}