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`}