diff --git a/src/js/common/components/Politician/PoliticianEndorsementsList.jsx b/src/js/common/components/Politician/PoliticianEndorsementsList.jsx index ad81871b6..c843aebf6 100644 --- a/src/js/common/components/Politician/PoliticianEndorsementsList.jsx +++ b/src/js/common/components/Politician/PoliticianEndorsementsList.jsx @@ -19,6 +19,7 @@ import { CampaignSubSectionTitleWrapper, } from '../Style/CampaignDetailsStyles'; import PoliticianStore from '../../stores/PoliticianStore'; +import VoterPositionEntryAndDisplay2 from '../../../components/PositionItem/VoterPositionEntryAndDisplay'; const STARTING_NUMBER_OF_POSITIONS_TO_DISPLAY = 2; const NUMBER_OF_POSITIONS_TO_ADD_WHEN_MORE_CLICKED = 10; @@ -112,7 +113,7 @@ class PoliticianEndorsementsList extends Component { render () { renderLog('PoliticianEndorsementsList'); // Set LOG_RENDER_EVENTS to log all renders const { politicianWeVoteId, hideEncouragementToEndorse, showTitle } = this.props; - const { filteredPositionList, numberOfPositionsToDisplay, politicianName } = this.state; + const { filteredPositionList, numberOfPositionsToDisplay, politicianName, showOpinionModal } = this.state; // console.log('PoliticianEndorsementsList render numberOfPositionsToDisplay:', numberOfPositionsToDisplay); const showTitleAndPositionsToShow = showTitle && (filteredPositionList && filteredPositionList.length > 0); const listTitleHtml = showTitleAndPositionsToShow && ( @@ -178,6 +179,11 @@ class PoliticianEndorsementsList extends Component { /> )} + {/* Render the modal */} + ); } diff --git a/src/js/components/Activity/ActivityPostAdd.jsx b/src/js/components/Activity/ActivityPostAdd.jsx index 68b5ae954..03a1806c5 100644 --- a/src/js/components/Activity/ActivityPostAdd.jsx +++ b/src/js/components/Activity/ActivityPostAdd.jsx @@ -1,9 +1,9 @@ +import React, { Component, Suspense } from 'react'; import { Card, InputBase } from '@mui/material'; import styled from 'styled-components'; import withStyles from '@mui/styles/withStyles'; import withTheme from '@mui/styles/withTheme'; import PropTypes from 'prop-types'; -import React, { Component, Suspense } from 'react'; import { isCordova } from '../../common/utils/isCordovaOrWebApp'; import { renderLog } from '../../common/utils/logging'; import VoterStore from '../../stores/VoterStore'; diff --git a/src/js/components/Activity/ActivityPostPublicDropdown.jsx b/src/js/components/Activity/ActivityPostPublicDropdown.jsx new file mode 100644 index 000000000..44775e9dc --- /dev/null +++ b/src/js/components/Activity/ActivityPostPublicDropdown.jsx @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Select, MenuItem, FormControl, Typography } from '@mui/material'; +import { withStyles } from '@mui/styles'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import DesignTokenColors from '../../common/components/Style/DesignTokenColors'; + +const ActivityPostPublicDropdown = (props) => { + const { visibilityIsPublic, onVisibilityChange, classes } = props; + + const handleVisibilityChange = (event) => { + const { value } = event.target; + onVisibilityChange(value === 'Public'); + }; + + return ( + +
+ + Opinion visible to: + + +
+
+ ); +}; + +ActivityPostPublicDropdown.propTypes = { + visibilityIsPublic: PropTypes.bool.isRequired, + onVisibilityChange: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, +}; + +const styles = (theme) => ({ + formControl: { + width: '100%', + }, + container: { + alignItems: 'center', + display: 'flex', + }, + label: { + color: DesignTokenColors.neutralUI900, + fontFamily: 'Poppins', + fontSize: '13px', + fontWeight: '400', + lineHeight: '19.5px', + marginRight: '8px', + [theme.breakpoints.down('md')]: { + display: 'none', + }, + }, + selectVisibility: { + border: 'none', + boxShadow: 'none', + color: DesignTokenColors.neutralUI900, + fontSize: '16px', + fontWeight: '400', + lineHeight: '21.82px', + outline: 'none', + padding: 0, + '&:focus': { + outline: 'none', + boxShadow: 'none', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + [theme.breakpoints.down('sm')]: { + padding: 0, + }, + [theme.breakpoints.down('xs')]: { + padding: '0 32px 0 0', + fontSize: '14px', + }, + }, + menuItem: { + color: DesignTokenColors.neutralUI900, + fontSize: '16px', + fontWeight: '400', + lineHeight: '21.82px', + padding: '10px', + [theme.breakpoints.down('xs')]: { + fontSize: '14px', + }, + }, + menuPaper: { + '& .MuiMenu-list': { + padding: 0, + }, + }, + outlinedInputRoot: { + padding: 0, + '& .MuiSelect-select': { + padding: '0 !important', + }, + }, +}); + +export default withStyles(styles)(ActivityPostPublicDropdown); diff --git a/src/js/components/PositionItem/VoterPositionEditNameAndPhotoModal.jsx b/src/js/components/PositionItem/VoterPositionEditNameAndPhotoModal.jsx new file mode 100644 index 000000000..a84cb0b72 --- /dev/null +++ b/src/js/components/PositionItem/VoterPositionEditNameAndPhotoModal.jsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import { Dialog, DialogTitle, DialogContent, IconButton } from '@mui/material'; +import { withStyles } from '@mui/styles'; +import PropTypes from 'prop-types'; +import { Close as CloseIcon } from '@mui/icons-material'; +import VoterStore from '../../stores/VoterStore'; +import SettingsProfilePicture from '../Settings/SettingsProfilePicture'; +import SettingsWidgetFirstLastName from '../Settings/SettingsWidgetFirstLastName'; +import SettingsWidgetOrganizationWebsite from '../Settings/SettingsWidgetOrganizationWebsite'; +import SettingsWidgetOrganizationDescription from '../Settings/SettingsWidgetOrganizationDescription'; +import SettingsWidgetAccountType from '../Settings/SettingsWidgetAccountType'; +import DesignTokenColors from '../../common/components/Style/DesignTokenColors'; + +const VoterPositionEditNameAndPhotoModal = ({ show, toggleModal, classes }) => { + const [voter, setVoter] = useState(null); + + useEffect(() => { + if (show) { + // Fetch voter data when modal is displayed + const voterData = VoterStore.getVoter(); + setVoter(voterData); + } + }, [show]); + + if (!voter) { + return null; // Prevent rendering until voter data is available + } + + return ( + + + Edit Profile + + + + + +

+ We are serious about protecting your information. We are a non-profit and will never sell your data. + {' '} + + Frequently Asked Questions. + +

+
+ {/* Profile picture */} + + + {/* First and last name */} + + + {/* Website */} + + + {/* Organization description */} + + + {/* Account type */} + +
+
+
+ ); +}; + +VoterPositionEditNameAndPhotoModal.propTypes = { + show: PropTypes.bool.isRequired, + toggleModal: PropTypes.func.isRequired, + classes: PropTypes.object.isRequired, +}; + +const styles = { + modalContent: { + padding: '20px', + }, + a: { + color: DesignTokenColors.primary500, + }, + closeButton: { + color: DesignTokenColors.neutral100, + position: 'absolute', + right: '8px', + top: '8px', + }, +}; + +export default withStyles(styles)(VoterPositionEditNameAndPhotoModal); diff --git a/src/js/components/PositionItem/VoterPositionEntryAndDisplay.jsx b/src/js/components/PositionItem/VoterPositionEntryAndDisplay.jsx new file mode 100644 index 000000000..5a2d52a5a --- /dev/null +++ b/src/js/components/PositionItem/VoterPositionEntryAndDisplay.jsx @@ -0,0 +1,254 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button, InputBase, Radio, FormControlLabel, RadioGroup } from '@mui/material'; +import { withStyles } from '@mui/styles'; +import PropTypes from 'prop-types'; +import { Edit as EditIcon } from '@mui/icons-material'; +import ActivityActions from '../../actions/ActivityActions'; +import { prepareForCordovaKeyboard } from '../../common/utils/cordovaUtils'; +import { isAndroid } from '../../common/utils/isCordovaOrWebApp'; +import { renderLog } from '../../common/utils/logging'; +import ActivityStore from '../../stores/ActivityStore'; +import VoterStore from '../../stores/VoterStore'; +import { avatarGeneric } from '../../utils/applicationUtils'; +import ModalDisplayTemplateB, { + templateBStyles, TextFieldDiv, + TextFieldForm, TextFieldWrapper, VoterAvatarImg, + UserInfoWrapper, UserInfoText, UserName, OptionBlockWrapper, CommentContainer, InputBox, +} from '../Widgets/ModalDisplayTemplateB'; +// import ActivityPostPublicToggle from '../Activity/ActivityPostPublicToggle'; +import ActivityPostPublicDropdown from '../Activity/ActivityPostPublicDropdown'; +import VoterPositionEditNameAndPhotoModal from './VoterPositionEditNameAndPhotoModal'; + +const VoterPositionEntryAndDisplay = (props) => { + const { activityTidbitWeVoteId, classes, externalUniqueId, toggleModal } = props; + + // useState used for state variables + const [visibilityIsPublic, setVisibilityIsPublic] = useState(false); + const [voterPhotoUrlMedium, setVoterPhotoUrlMedium] = useState(''); + const [statementText, setStatementText] = useState(''); + const [initialFocusSet, setInitialFocusSet] = useState(false); + const [voterName, setVoterName] = useState(''); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + + const handleEditModalOpen = () => { + setIsEditModalOpen(true); // Open the modal + }; + const handleEditModalClose = () => { + setIsEditModalOpen(false); // Close the modal + }; + + // useRef to reference the post input + const activityPostInputRef = useRef(null); + + const onActivityStoreChange = () => { + const activityPost = ActivityStore.getActivityTidbitByWeVoteId(activityTidbitWeVoteId); + if (activityPost) { + const { statement_text: newStatementText, visibility_is_public: newVisibilityIsPublic } = activityPost; + setVisibilityIsPublic(newVisibilityIsPublic); + setStatementText(newStatementText); + } + }; + + const onVoterStoreChange = () => { + const voter = VoterStore.getVoter(); + setVoterPhotoUrlMedium(voter.voter_photo_url_medium); + setVoterName(voter.full_name || 'Anonymous'); + }; + const [selectedOpinion, setSelectedOpinion] = useState(''); + + const handleOpinionChange = (event) => { + setSelectedOpinion(event.target.value); + }; + + + // useEffect replaces componentDidMount and componentWillUnmount + useEffect(() => { + const activityStoreListener = ActivityStore.addListener(onActivityStoreChange); + const voterStoreListener = VoterStore.addListener(onVoterStoreChange); + onActivityStoreChange(); + onVoterStoreChange(); + + return () => { + activityStoreListener.remove(); + voterStoreListener.remove(); + }; + }, []); + + // useEffect handles setting inital focus replacing componentDidUpdate + useEffect(() => { + if (activityPostInputRef.current && !initialFocusSet) { + const input = activityPostInputRef.current; + const { length } = input.value; + input.focus(); + input.setSelectionRange(length, length); + setInitialFocusSet(true); + } + }, [initialFocusSet]); + + const onFocusInput = () => { + prepareForCordovaKeyboard('VoterPositionEntryAndDisplay'); + }; + + const saveActivityPost = (e) => { + e.preventDefault(); + const visibilitySetting = visibilityIsPublic ? 'SHOW_PUBLIC' : 'FRIENDS_ONLY'; + ActivityActions.activityPostSave(activityTidbitWeVoteId, statementText, visibilitySetting); + toggleModal(); + }; + + const updateStatementTextToBeSaved = (e) => { + setStatementText(e.target.value); + }; + + const activityTidbitIdCheck = activityTidbitWeVoteId === '' || activityTidbitWeVoteId === undefined; + + renderLog('VoterPositionEntryAndDisplay'); // Set LOG_RENDER_EVENTS to log all renders + + const dialogTitleText = activityTidbitIdCheck ? 'Create post' : 'Edit post'; + const statementPlaceholderText = 'What\'s on your mind?'; + const rowsToShow = isAndroid() ? 4 : 6; + const [showModal, setShowModal] = useState(false); + + const toggleLocalModal = () => { + setShowModal((prev) => !prev); // Toggle the modal + }; + const OpinionBlock = ({ onClick }) => ( + + + + + + + {/* Open modal when input is clicked */} + + + + ); + + OpinionBlock.propTypes = { + onClick: PropTypes.func.isRequired, + }; + const textFieldJSX = ( + + + + + + + + {' '} + {voterName} + {/* Display the fetched name */} + + setVisibilityIsPublic(newVisibility)} + /> + + + + } + label="Endorsing" + classes={{ root: classes.radioLabel }} + /> + } + label="Opposing" + classes={{ root: classes.radioLabel }} + /> + } + label="Neutral" + classes={{ root: classes.radioLabel }} + /> + + + + + + + + ); + + return ( + <> + {dialogTitleText}} + show={showModal} + textFieldJSX={textFieldJSX} + toggleModal={toggleLocalModal} + /> + {isEditModalOpen && ( + + )} + + + ); +}; + +VoterPositionEntryAndDisplay.propTypes = { + activityTidbitWeVoteId: PropTypes.string, + classes: PropTypes.object, + externalUniqueId: PropTypes.string, + toggleModal: PropTypes.func.isRequired, +}; + +export default withStyles(templateBStyles)(VoterPositionEntryAndDisplay); diff --git a/src/js/components/Widgets/ModalDisplayTemplateB.jsx b/src/js/components/Widgets/ModalDisplayTemplateB.jsx new file mode 100644 index 000000000..b6f6fe061 --- /dev/null +++ b/src/js/components/Widgets/ModalDisplayTemplateB.jsx @@ -0,0 +1,378 @@ +import { Close } from '@mui/icons-material'; +import { Dialog, DialogContent, DialogTitle, Divider, IconButton, Button } from '@mui/material'; +import withStyles from '@mui/styles/withStyles'; +import withTheme from '@mui/styles/withTheme'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { hasIPhoneNotch, isAndroidSizeWide } from '../../common/utils/cordovaUtils'; +import { isAndroid, isCordova } from '../../common/utils/isCordovaOrWebApp'; +import { renderLog } from '../../common/utils/logging'; +import DesignTokenColors from '../../common/components/Style/DesignTokenColors'; + +class ModalDisplayTemplateB extends Component { + constructor(props) { + super(props); + this.state = { + }; + } + + render () { + renderLog('ModalDisplayTemplateB'); // Set LOG_RENDER_EVENTS to log all renders + const { + classes, dialogTitleJSX, externalUniqueId, show, tallMode, textFieldJSX, + } = this.props; + let dialogPaperCombined; + if (tallMode) { + dialogPaperCombined = { ...classes.dialogPaper, ...classes.dialogPaperAdditionTall }; + } else { + dialogPaperCombined = classes.dialogPaper; + } + // This template is used by other components like ActivityPostModal, and PositionStatementModal + return ( + this.props.toggleModal()} + open={show} + style={{ paddingTop: `${isCordova() ? '75px' : 'undefined'}` }} + > + + + + {dialogTitleJSX || <> </>} + + this.props.toggleModal()} + id={`closeModalDisplayTemplateB${externalUniqueId}`} + size="large" + > + + + + + + + + {textFieldJSX} + + + + ); + } +} +ModalDisplayTemplateB.propTypes = { + classes: PropTypes.object, + dialogTitleJSX: PropTypes.object, + externalUniqueId: PropTypes.string, + show: PropTypes.bool, + tallMode: PropTypes.bool, + textFieldJSX: PropTypes.object, + toggleModal: PropTypes.func.isRequired, +}; + +export const templateBStyles = (theme) => ({ + dialogTitle: { + padding: isAndroid() ? 8 : 'inherit', + margin: 0, + }, + opposingLabel: { + margin: '0 8px', + padding: '4px 8px', + }, + styledEditIcon: { + alignItems: 'center', + backgroundColor: DesignTokenColors.primary50, + borderRadius: '50%', + color: DesignTokenColors.neutral400, + cursor: 'pointer', + display: 'flex', + fontSize: '24px', + margin: '19px 0 0 -20px', + padding: '5px', + position: 'relative', + }, + dialogPaper: { + border: `1px solid ${DesignTokenColors.neutral100}`, + borderRadius: '30px', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + marginTop: hasIPhoneNotch() ? 68 : 48, + minHeight: isAndroid() ? '257px' : '200px', + maxWidth: '600px', + top: '50px', + width: '90%', + transform: isAndroid() ? 'translate(0%, -18%)' : 'translate(0%, -20%)', + [theme.breakpoints.down('xs')]: { + minWidth: '95%', + maxWidth: '95%', + width: '95%', + minHeight: isAndroid() ? '237px' : '200px', + maxHeight: '330px', + height: '70%', + margin: '0 auto', + transform: 'translate(0%, -30%)', + }, + [theme.breakpoints.up('sm')]: { + maxWidth: '500px', + width: '80%', + }, + [theme.breakpoints.up('md')]: { + maxWidth: '600px', + width: '70%', + }, + }, + dialogPaperAdditionTall: { + maxHeight: '550px', + [theme.breakpoints.down('xs')]: { + maxHeight: '530px', + }, + }, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + height: '100%', + closeButton: { + height: '24px', + position: 'absolute', + right: '16px', + width: '24.55px', + textAlign: 'left', + }, + formStyles: { + width: '100%', + }, + formControl: { + marginTop: 16, + width: '100%', + }, + inputMultiline: { + fontSize: 20, + height: '100%', + width: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: 18, + }, + }, + inputStyles: { + flex: '1 1 0', + fontSize: 18, + height: '100%', + width: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: 16, + }, + }, + select: { + padding: '12px 12px', + margin: '0 1px', + }, + saveButtonRoot: { + borderRadius: '30px', + color: 'white', + fontWeight: 600, + fontSize: 16, + height: '40px', + width: '100%', + lineHeight: '24px', + margin: '16px 0 0 0 ', + '&.Mui-disabled': { + backgroundColor: DesignTokenColors.neutral100, + color: DesignTokenColors.whiteUI, + opacity: 0.6, + }, + }, + radioGroup: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + gap: '16px', + marginBottom: '10px', + padding: '0', + width: '100%', + + '@media (max-width: 600px)': { + alignItems: 'flex-start', + flexDirection: 'column', + gap: '8px', + }, + + '@media (max-width: 375px)': { + alignItems: 'flex-start', + gap: '8px', + }, + }, + radioLabel: { + alignItems: 'center', + color: DesignTokenColors.neutral900, + display: 'flex', + fontWeight: '400', + lineHeight: '22px', + margin: 0, + [theme.breakpoints.down('sm')]: { + fontSize: '12px !important', + }, + }, + radioRoot: { + color: DesignTokenColors.neutral900, + }, + radioChecked: { + color: DesignTokenColors.primary600, + }, +}); + +const DialogContentInnerWrapper = styled('div')` + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; + margin-top:25px; + + @media (max-width: 600px) { + min-height: 48px; + } + + @media (min-width: 960px) { + min-height: 64px; + } +`; + +const DialogTitleInnerWrapper = styled('div')` + align-items: center; + display: flex; + justify-content: space-between; + min-height: 56px; + + @media ${(props) => props.theme.breakpoints.down('sm')} { + min-height: 48px; + } +`; + +export const horizontalEllipsis = '\u2026'; + +export const TextFieldDiv = styled('div')` + align-items: flex-start; + font-size: 16px; + border: 1px solid ${DesignTokenColors.primary600}; + border-radius: 16px; + display: flex; + margin-bottom: 0; + padding: ${isAndroidSizeWide() ? '12px 12px 0 12px' : '12px'}; +`; + +export const TextFieldForm = styled('form')` + height: 95%; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +export const TextFieldWrapper = styled('div')` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const Title = styled('div')` + font-size: 18px; + font-weight: bold; + line-height: 24.55px; + margin: 16px 0; + text-align: left; + padding-left: 20px; + + @media (max-width: 600px) { + font-size: 16px; + line-height: 22px; + } + + @media (min-width: 960px) { + font-size: 20px; + line-height: 28px; + } +`; +export const VoterAvatarImg = styled('img')` + border-radius: 50%; + width: 43px; + height: 43px; + display: block; + @media (max-width: 600px) { + margin-left: 0px; + } +`; + +export const UserInfoWrapper = styled('div')` + display: flex; +`; + +export const UserInfoText = styled('div')` + padding-left: 16px; + `; +export const UserName = styled('div')` + color: DesignTokenColors.neutralUI900; + font-size: 18px; + font-weight: 600; + line-height: 25px; +`; + +export const VisibilityText = styled('div')` + color: DesignTokenColors.neutral900; + font-size: 13px; + line-height: 20px; + margin-top: 4px; +`; + +export const OpinionButtonsWrapper = styled('div')` + display: flex; + justify-content: space-around; + padding: 12px 0; +`; + +export const OpinionButton = styled(Button)` + border-radius: 16px; + flex: 1; + font-size: 16px; + font-weight: 400; + line-height: 22px; + margin: 0 4px; + text-transform: capitalize; + + @media (max-width: 600px) { + font-size: 14px; + line-height: 20px; + } + + color: ${(props) => (props.selected ? DesignTokenColors.whiteUI : DesignTokenColors.primary600)}; + background-color: ${(props) => (props.selected ? DesignTokenColors.primary600 : 'transparent')}; + + &:hover { + background-color: ${(props) => (props.selected ? DesignTokenColors.primary600 : 'transparent')}; + background-color: ${(props) => (props.selected ? DesignTokenColors.primary600 : DesignTokenColors.primary50)}; + } +`; +export const OptionBlockWrapper = styled.div` + display: flex; + align-items: center; + gap: 10px; + margin: 16px 0; +`; +export const CommentContainer = styled.div` + flex-grow: 1; + background-color: ${DesignTokenColors.whiteUI}; + border: 1px solid ${DesignTokenColors.neutralUI300}; + border-radius: 16px; + padding: 8px 12px; + display: flex; + align-items: center; +`; + +export const InputBox = styled.input` + border: none; + color: ${DesignTokenColors.neutral900}; + font-size: 16px; + outline: none; + flex-grow: 1; +`; +export default withTheme(withStyles(templateBStyles)(ModalDisplayTemplateB));