fix: candidate modals

pull/89/head
Pierre-Louis Guhur 1 year ago
parent e05ddea3b6
commit 35a66d55d6

@ -3,13 +3,17 @@
*/
import {useState} from 'react'
import Image from 'next/image'
import TrashButton from "./TrashButton";
import {Row, Col} from "reactstrap";
import {useTranslation} from "next-i18next";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faPlus,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import {Row, Col} from "reactstrap";
import {useElection, useElectionDispatch} from './ElectionContext';
import defaultAvatar from '../../public/avatar.svg'
import addIcon from '../../public/add.svg'
import CandidateModal from './CandidateModal';
import CandidateModalSet from './CandidateModalSet';
import CandidateModalDel from './CandidateModalDel';
const CandidateField = ({position, className, ...inputProps}) => {
@ -21,41 +25,46 @@ const CandidateField = ({position, className, ...inputProps}) => {
const image = candidate && candidate.image ? candidate.image : defaultAvatar;
const active = candidate && candidate.active === true
const [modal, setModal] = useState(false);
const [modalDel, setModalDel] = useState(false);
const [modalSet, setModalSet] = useState(false);
const addCandidate = () => {
dispatch({'type': 'candidate-push', 'value': "default"})
};
const removeCandidate = () => {
dispatch({'type': 'candidate-rm', 'value': position})
}
const toggle = () => setModal(m => !m)
const toggleSet = () => setModalSet(m => !m)
const toggleDel = () => setModalDel(m => !m)
return (
<Row
className={`${className} p-2 my-3 border border-dashed border-dark border-opacity-50 align-items-center ${active ? "active" : ""}`}
className={`${className || ""} p-2 my-3 border border-dashed border-dark border-opacity-50 align-items-center ${active ? "active" : ""}`}
{...inputProps}
>
<Col onClick={toggle} className='col-auto me-auto'>
<Col onClick={toggleSet} className='cursor-pointer col-auto me-auto'>
<Row className='gx-3'>
<Col className='col-auto'>
<Image fill src={image} className={image == defaultAvatar ? "default-avatar" : ""} alt={t('common.thumbnail')} />
<Image src={image} width={24} height={24} className={image == defaultAvatar ? "default-avatar" : ""} alt={t('common.thumbnail')} />
</Col>
<Col className='col-auto fw-bold'>
{t("admin.add-candidate")}
</Col>
</Row>
</Col>
<Col className='col-auto'>
<Col className='col-auto cursor-pointer'>
{active ?
<div className={trashIcon}><TrashButton onClick={removeCandidate} /></div> :
<Image src={addIcon} onClick={addCandidate} alt={t('admin.add-candidate')} />
<FontAwesomeIcon
icon={faTrashCan}
onClick={() => setModalDel(m => !m)}
/> :
<FontAwesomeIcon
icon={faPlus}
onClick={addCandidate}
/>
}
</Col>
<CandidateModal toggle={toggle} isOpen={modal} position={position} />
<CandidateModalSet toggle={toggleSet} isOpen={modalSet} position={position} />
<CandidateModalDel toggle={toggleDel} isOpen={modalDel} position={position} />
</Row >
);
}

@ -1,123 +0,0 @@
import {
Row,
Col,
Label,
Input,
InputGroup,
Button,
Modal,
ModalBody,
Form
} from "reactstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faPlus,
faArrowLeft,
} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "next-i18next";
import Image from 'next/image';
import {useElection, useElectionDispatch} from './ElectionContext';
import defaultAvatar from '../../public/default-avatar.svg'
const CandidateModal = ({isOpen, position, toggle}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
const image = candidate && candidate.image ? candidate.image : defaultAvatar;
const addCandidate = () => {
dispatch({'type': 'candidate-push', 'value': "default"})
};
return (
<Modal
isOpen={isOpen}
toggle={toggle}
keyboard={true}
>
<div className="modal-header p-4">
<h4 className="modal-title">
{t('admin.add-candidate')}
</h4>
<button type="button" onClick={toggle} className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<ModalBody className='p-4'>
<p>{t('admin.add-candidate-desc')}
</p>
<Col>
<InputGroup>
<Form className='container container-fluid'>
<Row className='gx-4 mb-3'>
<Col className='col-auto'>
<Image src={image} height={120} width={120} alt={t('admin.photo')} />
</Col>
<Col className='col-auto'>
<Label className='fw-bold'>{t('admin.photo')} <span className='text-muted'> ({t('admin.optional')})</span></Label>
<p>{t('admin.photo-type')} jpg, png, pdf</p>
<div>
<input type="file" name="image-upload" id="image-upload" />
<label className="inputfile" htmlfor="image-upload">{t('admin.photo-import')}</label>
</div>
</Col>
</Row>
<div className='mb-3'>
<Label className='fw-bold'>{t('common.name')} </Label>
<Input
type="text"
placeholder={t("admin.candidate-name-placeholder")}
tabIndex={position + 1}
maxLength="250"
autoFocus
required
/>
</div>
<div className=''>
<Label className='fw-bold'>{t('common.description')} <span className='text-muted'> ({t('admin.optional')})</span></Label>
<Input
type="text"
defaultValue={candidate.description}
placeholder={t("admin.candidate-desc-placeholder")}
maxLength="250"
/>
</div>
<Row className='mt-5 mb-3'>
<Col className='col-auto me-auto'>
<Button onClick={toggle} color='dark' outline={true}>
<Row className='gx-2 align-items-end'>
<Col>
<FontAwesomeIcon icon={faArrowLeft} />
</Col>
<Col>
{t('common.cancel')}
</Col>
</Row>
</Button>
</Col>
<Col className='col-auto '>
<Button outline={true} color="primary" onClick={addCandidate}>
<Row className='gx-2 align-items-end'>
<Col>
<FontAwesomeIcon icon={faPlus} />
</Col>
<Col>
<span>{t('common.save')}</span>
</Col>
</Row>
</Button>
</Col>
</Row>
</Form>
</InputGroup>
</Col>
</ModalBody >
</Modal >);
}
export default CandidateModal;

@ -0,0 +1,86 @@
import {
Row,
Col,
Label,
Input,
Button,
Modal,
ModalBody,
Form
} from "reactstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faTrashCan,
faTrashAlt,
faArrowLeft,
} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "next-i18next";
import Image from 'next/image';
import {useElection, useElectionDispatch} from './ElectionContext';
import {upload} from '@services/imgpush';
import {IMGPUSH_URL} from '@services/constants';
import defaultAvatar from '../../public/default-avatar.svg'
import {useEffect} from "react";
const CandidateModal = ({isOpen, position, toggle}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
const removeCandidate = () => {
dispatch({'type': 'candidate-rm', 'position': position})
}
return (
<Modal
isOpen={isOpen}
toggle={toggle}
keyboard={true}
className='modal_candidate'
>
<ModalBody className='flex-column justify-contenter-center d-flex p-4'>
<Row className='justify-content-center'>
<Col className='col-auto px-4 py-4 rounded-circle bg-light'>
<FontAwesomeIcon size="2x" icon={faTrashCan} />
</Col>
</Row>
<p className='text-danger fw-bold text-center mt-4'>{t('admin.candidate-confirm-del')}
</p>
{candidate.name ? <h4 className='text-center'>{candidate.name}</h4> : null}
<Row className='mt-5 mb-3'>
<Col className='col-auto me-auto'>
<Button onClick={toggle} color='dark' outline={true}>
<Row className='gx-2 align-items-end'>
<Col className='col-auto'>
<FontAwesomeIcon icon={faArrowLeft} />
</Col>
<Col className='col-auto'>
{t('admin.candidate-confirm-back')}
</Col>
</Row>
</Button>
</Col>
<Col className='col-auto '>
<Button outline={true} color="primary" onClick={removeCandidate}>
<Row className='gx-2 align-items-end'>
<Col className='col-auto'>
<FontAwesomeIcon icon={faTrashAlt} />
</Col>
<Col className='col-auto'>
{t('admin.candidate-confirm-ok')}
</Col>
</Row>
</Button>
</Col>
</Row >
</ModalBody >
</Modal >);
}
export default CandidateModal;

@ -0,0 +1,180 @@
import {useState, useEffect, useRef} from 'react'
import {
Row,
Col,
Label,
Input,
Button,
Modal,
ModalBody,
Form
} from "reactstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faPlus,
faArrowLeft,
} from "@fortawesome/free-solid-svg-icons";
import {useTranslation} from "next-i18next";
import Image from 'next/image';
import {useElection, useElectionDispatch} from './ElectionContext';
import {upload} from '@services/imgpush';
import {IMGPUSH_URL} from '@services/constants';
import defaultAvatar from '../../public/default-avatar.svg'
const CandidateModal = ({isOpen, position, toggle}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
const [state, setState] = useState(candidate);
console.log('state', state);
const image = state.image && state.image != "" ? state.image : defaultAvatar;
const handleFile = async (event) => {
const payload = await upload(event.target.files[0])
setState(s => ({...s, "image": `${IMGPUSH_URL}/${payload['filename']}`}))
}
// to manage the hidden ugly file input
const hiddenFileInput = useRef(null);
useEffect(() => {
setState(election.candidates[position]);
console.log('effect election', election)
}, [election])
const save = () => {
dispatch({
'type': 'candidate-set',
'position': position,
'field': "image",
'value': state.image,
})
dispatch({
'type': 'candidate-set',
'position': position,
'field': "name",
'value': state.name,
})
dispatch({
'type': 'candidate-set',
'position': position,
'field': "description",
'value': state.description,
})
toggle();
}
const handleName = (e) => {
setState(s => ({...s, 'name': e.target.value}))
}
const handleDescription = (e) => {
setState(s => ({...s, 'description': e.target.value}))
}
return (
<Modal
isOpen={isOpen}
toggle={toggle}
keyboard={true}
className='modal_candidate'
>
<div className="modal-header p-4">
<h4 className="modal-title">
{t('admin.add-candidate')}
</h4>
<button type="button" onClick={toggle} className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<ModalBody className='p-4'>
<p>{t('admin.add-candidate-desc')}
</p>
<Col>
<Form className='container container-fluid'>
<Row className='gx-4 mb-3'>
<Col className='col-auto'>
<Image src={image} alt={t('admin.photo')} height={120} width={120} />
</Col>
<Col className='col-auto'>
<Label className='fw-bold'>{t('admin.photo')} <span className='text-muted'> ({t('admin.optional')})</span></Label>
<p>{t('admin.photo-type')} jpg, png, pdf</p>
<div>
<input
type="file"
className='hide'
onChange={handleFile}
ref={hiddenFileInput}
/>
<Button
color='dark'
outline={true}
onClick={() => hiddenFileInput.current.click()}
>
{t('admin.photo-import')}
</Button>
</div>
</Col>
</Row>
<div className='mb-3'>
<Label className='fw-bold'>{t('common.name')} </Label>
<Input
type="text"
placeholder={t("admin.candidate-name-placeholder")}
tabIndex={position + 1}
value={state.name}
onChange={handleName}
maxLength="250"
autoFocus
required
/>
</div>
<div className=''>
<Label className='fw-bold'>{t('common.description')} <span className='text-muted'> ({t('admin.optional')})</span></Label>
<Input
type="text"
defaultValue={candidate.description}
placeholder={t("admin.candidate-desc-placeholder")}
onChange={handleDescription}
value={state.description}
maxLength="250"
/>
</div>
<Row className='mt-5 mb-3'>
<Col className='col-auto me-auto'>
<Button onClick={toggle} color='dark' outline={true}>
<Row className='gx-2 align-items-end'>
<Col>
<FontAwesomeIcon icon={faArrowLeft} />
</Col>
<Col>
{t('common.cancel')}
</Col>
</Row>
</Button>
</Col>
<Col className='col-auto '>
<Button outline={true} color="primary" onClick={save}>
<Row className='gx-2 align-items-end'>
<Col>
<FontAwesomeIcon icon={faPlus} />
</Col>
<Col>
{t('common.save')}
</Col>
</Row>
</Button>
</Col>
</Row>
</Form>
</Col>
</ModalBody >
</Modal >);
}
export default CandidateModal;

@ -76,15 +76,29 @@ function electionReducer(election, action) {
}
case 'candidate-push': {
const candidate = action.value === 'default' ? {...defaultCandidate} : action.value;
election.candidates.push(candidate)
return election;
const candidates = [...election.candidates, candidate];
return {...election, candidates}
}
case 'candidate-rm': {
if (typeof action.value !== "number") {
if (typeof action.position !== "number") {
throw Error(`Unexpected candidate position ${action.position}`)
}
const candidates = [...election.candidates];
candidates.splice(action.position)
return {...election, candidates}
}
case 'candidate-set': {
if (typeof action.position !== "number") {
throw Error(`Unexpected candidate position ${action.value}`)
}
election.candidates.split(action.value)
return election;
if (action.field === "active") {
throw Error("You are not allowed the set the active flag")
}
const candidates = [...election.candidates];
const candidate = candidates[action.position]
candidate[action.field] = action.value
candidate['active'] = true;
return {...election, candidates}
}
default: {
throw Error(`Unknown action: ${action.type}`);

@ -1,5 +1,16 @@
const {i18n} = require('./next-i18next.config.js')
const remoteImage = process.env.IMGPUSH_URL ? process.env.IMGPUSH_URL.split('/')[-1] : "imgpush.mieuxvoter.fr";
module.exports = {
i18n,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: remoteImage,
pathname: '**',
},
],
}
};

9676
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -10,13 +10,12 @@
"export": "next export"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@fortawesome/fontawesome-free": "^6.2.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@svgr/webpack": "^6.5.1",
"babel-eslint": "^10.1.0",
"bootstrap": "^5.2.2",
"bootstrap-scss": "^5.2.2",

@ -63,7 +63,7 @@ const CreateElectionForm = (props) => {
<ElectionProvider>
<CreationSteps step={step} className='m-5 justify-content-center d-flex' />
<CandidatesField />
<Form />
</ElectionProvider>
);

@ -195,7 +195,7 @@ const ShareRow = () => {
const Home = () => {
const {t} = useTranslation('resource');
return (
<Container className="homePage">
<Container fluid={true} className='p-0'>
<section><StartForm /></section>
<section className="sectionTwoHome">
<AdvantagesRow />

@ -47,7 +47,10 @@
"admin.step-params": "Parameters",
"admin.step-confirm": "Confirm",
"admin.add-candidates": "Add the candidates.",
"admin.add-candidate": "Add a candidate.",
"admin.add-candidate": "Add a candidate",
"admin.candidate-confirm-del": "You want to remove a candidate",
"admin.candidate-confirm-back": "No, I keep it",
"admin.candidate-confirm-ok": "Delete",
"admin.add-candidate-desc": "Add a picture, a name, and a description of the candidate.",
"admin.candidate-name-placeholder": "Add the name or the title of the candidate.",
"admin.candidate-desc-placeholder": "Add the description of the candidate.",

@ -47,10 +47,13 @@
"admin.step-params": "Paramètres du vote",
"admin.step-confirm": "Confirmation",
"admin.add-candidates": "Ajouter les candidats.",
"admin.add-candidate": "Ajouter un candidat.",
"admin.add-candidate": "Ajouter un candidat",
"admin.candidate-name-placeholder": "Ajouter le nom ou le titre du candidat.",
"admin.candidate-desc-placeholder": "Ajouter la description du candidat.",
"admin.add-candidate-desc": "Ajouter une photo, le nom et une description au candidat.",
"admin.candidate-confirm-del": "Vous souhaitez supprimer un candidat",
"admin.candidate-confirm-back": "Non, je le garde",
"admin.candidate-confirm-ok": "Supprimer",
"admin.photo": "Photo",
"admin.optional": "facultatif",
"admin.photo-import": "Importer une photo",

@ -5,3 +5,4 @@
export const MAX_NUM_CANDIDATES = process.env.MAX_NUM_CANDIDATES || 1000;
export const CONTACT_MAIL = process.env.CONTACT_MAIL || "app@mieuxvoter.fr";
export const DEFAULT_GRADES = process.env.DEFAULT_GRADES || ['grades.very-good', 'grades.good', 'grades.passable', 'grades.inadequate', 'grades.mediocre'];
export const IMGPUSH_URL = process.env.IMGPUSH_URL || 'https://imgpush.mieuxvoter.fr';

@ -0,0 +1,16 @@
/**
* This is a mini-SDK to submit files upload to imgpush
*/
import {IMGPUSH_URL} from '@services/constants';
export const upload = async (photo) => {
const formData = new FormData();
formData.append('file', photo);
formData.append('fileName', photo.name);
return fetch(
IMGPUSH_URL,
{method: "POST", body: formData}
)
.then(ans => {return ans.json();})
}

@ -35,26 +35,41 @@
margin-right: 10px;
}
input[type="file"] {
display: none;
}
.inputfile {
background: transparent;
color: #0a004c;
border: 2px solid #0a004c;
box-shadow: 0px 2px 0px 0px #0a004c;
padding: 8px 20px;
font-size: 14px;
line-height: 24px;
display: inline-block;
cursor: pointer;
.creation-steps * > input[type="file"] {
display: none;
}
.creation-steps * > .inputfile {
background: transparent;
color: #0a004c;
border: 2px solid #0a004c;
box-shadow: 0px 2px 0px 0px #0a004c;
padding: 8px 20px;
font-size: 14px;
line-height: 24px;
display: inline-block;
cursor: pointer;
}
// .creation-steps > * .form-control, .creation-steps * > input[type='text']:focus,.creation-steps * > input[type='text']::placeholder {
.modal_candidate > * :is(
input[type='text'],
input[type='text']:focus,
input[type='text']::placeholder)
{
background: white;
border-radius: 0px;
color: black!important;
opacity: 0.5;
box-shadow: 0px 2px 0px #C3BFD8;
margin-bottom: 20px;
}
input[type='text'], input[type='text']:focus {
background: white;
border-radius: 0px;
color: black;
opacity: 0.8;
box-shadow: 0px 2px 0px #C3BFD8;
margin-bottom: 20px;
.modal_candidate > * input[type='text']:focus
{
opacity: 0.8;
}
.modal_candidate > * input[type='text']:placeholder
{
opacity: 0.4;
}

@ -358,12 +358,13 @@ h5 {
.form-control {
height: auto;
background: transparent;
color: white;
// color: white;
border: none;
}
/*
.form-control::placeholder {
color: white;
}
}*/
.btnTrash {
background-color: transparent;
color: white;
@ -400,3 +401,11 @@ ol.result > li {
color: inherit;
opacity: 0.5;
}
.cursor-pointer {
cursor: pointer;
}
.hide {
display: none;
}

@ -5,10 +5,6 @@ $laptop: 1440px;
$desktop: 1680px;
/** HOMEPAGE **/
.homePage {
max-width: 100% !important;
padding: 0px;
}
.sectionOneHomeForm {
max-width: 100% !important;
background-color: #2400fd;
@ -31,7 +27,6 @@ $desktop: 1680px;
}
.sectionOneHomeInput {
height: 48px;
background: $mv-blue-color;
box-shadow: 0px 2px 0px rgba(255, 255, 255, 0.32);
border: none;
margin: 8px 0px;

Loading…
Cancel
Save