fix: refactor candidate

pull/89/head
Pierre-Louis Guhur 1 year ago
parent c8ae7147e6
commit 2c30adaeb9

@ -0,0 +1,39 @@
/**
* This component displays a bar releaving the current step
*/
import {useTranslation} from "next-i18next";
const {Row, Col} = require("reactstrap")
const Step = ({name, position, active}) => {
const {t} = useTranslation();
return <Col
className="col-auto">
<Row className={`align-items-center creation-step ${active ? 'active' : ''}`}>
<Col className='col-auto badge align-items-center justify-content-center d-flex'>
<div>{position}</div>
</Col>
<Col className='col-auto name'>
{t(`admin.step-${name}`)}
</Col>
</Row>
</Col >
}
const CreationSteps = ({step, ...props}) => {
const steps = ['candidate', 'params', 'confirm'];
if (!steps.includes(step)) {
throw Error(`Unknown step {step}`);
}
return <div {...props}>
<Row className='justify-content-between creation-steps'>
{steps.map((name, i) => <Step name={name} active={step === name} key={i} position={i + 1} />
)}
</Row >
</div >
}
export default CreationSteps;

@ -0,0 +1,25 @@
/**
* A toggle button using bootstrap
*/
const Toggle = ({active, children}) => {
return (<button
type="button"
className={`btn btn-toggle ${active ? 'active' : ''}`}
data-toggle="button"
aria-pressed="false"
autocomplete="off"
>
{children}
</button>
)
}
Toggle.defaultProps = {
'active': false
}
export default Toggle;

@ -0,0 +1,63 @@
/**
* This is the candidate field used during election creation
*/
import {useState} from 'react'
import Image from 'next/image'
import TrashButton from "./TrashButton";
import {Row, Col} from "reactstrap";
import {useTranslation} from "react-i18next";
import {useElection, useElectionDispatch} from './ElectionContext';
import defaultAvatar from '../../public/avatar.svg'
import addIcon from '../../public/add.svg'
import CandidateModal from './CandidateModal';
const CandidateField = ({position, className, ...inputProps}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
const image = candidate && candidate.image ? candidate.image : defaultAvatar;
const active = candidate && candidate.active === true
const [modal, setModal] = useState(false);
const addCandidate = () => {
dispatch({'type': 'candidate-push', 'value': "default"})
};
const removeCandidate = () => {
dispatch({'type': 'candidate-rm', 'value': position})
}
const toggle = () => setModal(m => !m)
return (
<Row
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'>
<Row className='gx-3'>
<Col className='col-auto'>
<Image fill src={image} 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'>
{active ?
<div className={trashIcon}><TrashButton onClick={removeCandidate} /></div> :
<Image src={addIcon} onClick={addCandidate} alt={t('admin.add-candidate')} />
}
</Col>
<CandidateModal toggle={toggle} isOpen={modal} position={position} />
</Row >
);
}
export default CandidateField;

@ -0,0 +1,117 @@
import {
Row,
Col,
Label,
Input,
InputGroup,
InputGroupAddon,
Button, Modal, ModalHeader, ModalBody, Form
} from "reactstrap";
import {useTranslation} from "react-i18next";
import Image from 'next/image';
import {
faPlus, faCogs, faCheck, faTrash
} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {useElection, useElectionDispatch} from './ElectionContext';
import ButtonWithConfirm from "./ButtonWithConfirm";
import defaultAvatar from '../../public/avatar.svg'
import HelpButton from "@components/admin/HelpButton";
import AddPicture from "@components/admin/AddPicture";
import addIcon from '../../public/add.svg'
import leftArrowIcon from '../../public/arrow-left.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 removeCandidate = () => {
dispatch({'type': 'candidate-rm', 'value': position})
}
const addCandidate = () => {
dispatch({'type': 'candidate-push', 'value': "default"})
};
return (
<Modal
isOpen={isOpen}
toggle={toggle}
keyboard={true}
>
<div className="modal-header">
<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>
<p>{t('admin.add-candidate-desc')}
</p>
<Col className="addCandidateCard">
<InputGroup className="addCandidateForm">
<Form>
<div className="input-group-prepend">
<div className="ajout-avatar">
<div>
<div className="avatar-placeholer">
</div>
</div>
<div className="avatar-text">
<h4>{t('admin.photo')} <span> ({t('admin.optional')})</span></h4>
<p>{t('admin.photo-type')} jpg, png, pdf</p>
<div className="btn-ajout-avatar">
<input type="file" name="myImage" id="myImage" />
<label className="inputfile" htmlFor="myImage">{t('admin.photo-import')}</label>
</div>
</div>
</div>
<img src="/avatar.svg" />
</div>
<Label className="addCandidateText">{t('common.name')}</Label>
<Input
type="text"
placeholder={t("admin.candidate-name-placeholder")}
tabIndex={position + 1}
maxLength="250"
autoFocus
className="addCandidateText"
required
/>
<Label>{t('common.description')} <span> ({t('admin.optional')})</span></Label>
<Input
type="text"
defaultValue={candidate.description}
placeholder={t("admin.candidate-desc-placeholder")}
maxLength="250"
/>
<Row>
<Col col='col-auto me-auto'>
<Button onClick={toggle} color='dark' outline={true}>
<Image src={leftArrowIcon} alt={t('common.cancel')} />
{t('common.cancel')}
</Button>
</Col>
<Col col='col-auto '>
<Button outline={true} color="primary" onClick={addCandidate} className="p-3">
<Image src={addIcon} alt={t('common.save')} />
<span>{t('common.save')}</span>
</Button>
</Col>
</Row>
</Form>
</InputGroup>
</Col>
</ModalBody>
</Modal >);
}
export default CandidateModal;

@ -0,0 +1,47 @@
import {useState, useEffect, createRef} from 'react'
import {useTranslation} from "react-i18next";
import CandidateField from './CandidateField'
import Alert from '@components/Alert'
import {MAX_NUM_CANDIDATES} from '@services/constants';
import {Container} from 'reactstrap';
import {useElection, useElectionDispatch} from './ElectionContext';
const CandidatesField = () => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidates = election.candidates;
const [error, setError] = useState(null)
// What to do when we change the candidates
useEffect(() => {
// Initialize the list with at least two candidates
if (candidates.length < 2) {
dispatch({'type': 'candidate-push', 'value': "default"})
}
if (candidates.length > MAX_NUM_CANDIDATES) {
setError('error.too-many-candidates')
}
}, [candidates])
return (
<Container className="candidate mt-5">
<h4 className='mb-4'>{t('admin.add-candidates')}</h4>
<Alert msg={error} />
{candidates.map((candidate, index) => {
return (
<CandidateField
key={index}
position={index}
/>
)
})}
</Container>
);
}
export default CandidatesField

@ -0,0 +1,30 @@
/**
* This component manages the date for ending the election
*/
import {useState} from 'react';
import {Row} from 'reactstrap';
import {useTranslation} from "next-i18next";
import {useElection, useElectionDispatch} from './ElectionContext';
import Toggle from '@components/Toggle';
const DateField = () => {
const election = useElection();
const dispatch = useElectionDispatch();
const [toggle, setToggle] = useState(false)
const {t} = useTranslation();
return (<Row>
<Col className='col-auto me-auto'>
{t('admin.date-limit')}
</Col>
<Col className='col-auto'>
<Toggle onChange={setToggle(t => !t)} />
</Col>
</Row>)
}
export default DateField;

@ -1,63 +1,163 @@
import {createContext, useContext, useReducer} from 'react';
/**
* This file provides a context and a reducer to manage an election
*/
import {createContext, useContext, useReducer, useEffect} from 'react';
import {useRouter} from "next/router";
import {DEFAULT_NUM_GRADES} from '@services/constants';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
// Store data about an election
const ElectionContext = createContext(null);
// Store the dispatch function that can modify an election
const ElectionDispatchContext = createContext(null);
export function TasksProvider({children}) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
export function ElectionProvider({children}) {
/**
* Provide the election and the dispatch to all children components
*/
const [election, dispatch] = useReducer(
electionReducer,
initialElection
);
// At the initialization, set the title using GET param
const router = useRouter();
useEffect(() => {
if (!router.isReady) return;
dispatch({
'type': 'set',
'field': 'title',
'value': router.query.title || ""
})
}, [router.isReady]);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider
<ElectionContext.Provider value={election}>
<ElectionDispatchContext.Provider
value={dispatch}
>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
</ElectionDispatchContext.Provider>
</ElectionContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
export function useElection() {
/**
* A simple hook to read the election
*/
return useContext(ElectionContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
export function useElectionDispatch() {
/**
* A simple hook to modify the election
*/
return useContext(ElectionDispatchContext);
}
function tasksReducer(tasks, action) {
function electionReducer(election, action) {
/**
* Manage all types of action doable on an election
*/
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
case 'set': {
election[action.field] = action.value;
return election;
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
case 'commit': {
throw Error('Not implemented yet')
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
case 'remove': {
throw Error('Not implemented yet')
}
case 'candidate-push': {
const candidate = action.value === 'default' ? {...defaultCandidate} : action.value;
election.candidates.push(candidate)
return election;
}
case 'candidate-rm': {
if (typeof action.value !== "number") {
throw Error(`Unexpected candidate position ${action.value}`)
}
election.candidates.split(action.value)
return election;
}
default: {
throw Error('Unknown action: ' + action.type);
throw Error(`Unknown action: ${action.type}`);
}
}
}
const initialTasks = [
{id: 0, text: 'Philosophers Path', done: true},
{id: 1, text: 'Visit the temple', done: false},
{id: 2, text: 'Drink matcha', done: false}
];
const defaultCandidate = {
name: "",
description: "",
active: false,
}
const initialElection = {
title: "",
description: "",
candidates: [{...defaultCandidate}, {...defaultCandidate}],
grades: DEFAULT_NUM_GRADES,
isTimeLimited: false,
isRandomOrder: false,
restrictResult: false,
restrictVote: false,
startVote: null,
endVote: null,
emails: [],
};
// const checkFields = () => {
// const numCandidates = candidates ? candidates.filter(c => c.label !== '') : 0;
// if (numCandidates < 2) {
// return {ok: false, msg: 'error.at-least-2-candidates'};
// }
//
// if (!title || title === "") {
// return {ok: false, msg: 'error.no-title'};
// }
//
// return {ok: true, msg: "OK"};
// };
//
// const handleSubmit = () => {
// const check = checkFields();
// if (!check.ok) {
// toast.error(t(check.msg), {
// position: toast.POSITION.TOP_CENTER,
// });
// return;
// }
//
// setWaiting(true);
//
// createElection(
// title,
// candidates.map((c) => c.label).filter((c) => c !== ""),
// {
// mails: emails,
// numGrades,
// start: start.getTime() / 1000,
// finish: finish.getTime() / 1000,
// restrictResult: restrictResult,
// restrictVote: restrictVote,
// locale: router.locale.substring(0, 2).toLowerCase(),
// },
// (result) => {
// if (result.id) {
// router.push(`/new/confirm/${result.id}`);
// } else {
// toast.error(t("Unknown error. Try again please."), {
// position: toast.POSITION.TOP_CENTER,
// });
// setWaiting(false);
// }
// }
// );
// };

@ -0,0 +1,13 @@
/**
* This component manages the title of the election
*/
import {useElection, useElectionDispatch} from './ElectionContext';
const TitleField = () => {
const election = useElection();
const dispatch = useElectionDispatch();
}
export default TitleField;

@ -6,7 +6,7 @@ import {Button, Modal, ModalHeader, ModalBody, ModalFooter} from "reactstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {useTranslation} from "next-i18next";
const TrashButton = ({className, label, onDelete}) => {
const TrashButton = ({className, label, onClick}) => {
const [visibled, setVisibility] = useState(false);
const {t} = useTranslation();
@ -14,7 +14,7 @@ const TrashButton = ({className, label, onDelete}) => {
return (
<div className="input-group-append cancelButton">
<FontAwesomeIcon onClick={toggle} icon={faTrashAlt} className="mr-2 cursorPointer" />
<FontAwesomeIcon onClick={toggle} icon={faTrashAlt} className="mr-2 cursorPointer" />
<Modal
isOpen={visibled}
toggle={toggle}
@ -34,14 +34,14 @@ const TrashButton = ({className, label, onDelete}) => {
type="button"
className={className}
onClick={toggle}>
<div className="annuler"><img src="/arrow-dark-left.svg" /> {t("No")}</div>
<div className="annuler"><img src="/arrow-dark-left.svg" /> {t("No")}</div>
</Button>
<Button
className="new-btn-confirm"
onClick={() => {toggle(); onDelete();}}
className="new-btn-confirm"
onClick={() => {toggle(); onClick();}}
>
<FontAwesomeIcon icon={faTrashAlt} className="mr-2"/>
<FontAwesomeIcon icon={faTrashAlt} className="mr-2" />
{t("Yes")}
</Button>
</ModalFooter>

@ -1,170 +0,0 @@
// import {useState, useEffect} from 'react'
// import ButtonWithConfirm from "./ButtonWithConfirm";
// import TrashButton from "./TrashButton";
// import {
// Row,
// Col,
// Label,
// Input,
// InputGroup,
// InputGroupAddon,
// Button, Modal, ModalHeader, ModalBody, Form
// } from "reactstrap";
// import {useTranslation} from "react-i18next";
// import HelpButton from "@components/form/HelpButton";
// import AddPicture from "@components/form/AddPicture";
// import {
// faPlus, faCogs, faCheck, faTrash
// } from "@fortawesome/free-solid-svg-icons";
// import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
// const DragHandle = sortableHandle(({children}) => (
// <span className="input-group-text indexNumber">{children}</span>
// ));
// const CandidateField = ({avatar, label, description, candIndex, onDelete, onAdd, ...inputProps}) => {
const CandidateField = (props) => {
// const {t} = useTranslation();
// const [visibled, setVisibility] = useState(false);
// const toggle = () => setVisibility(!visibled)
// const [selected, setSelectedState] = useState(false);
// const [className, setClassName] = useState("none");
// const [trashIcon, setTrashIcon] = useState("none");
// const [plusIcon, setPlusIcon] = useState("none");
// const addCandidate = () => {
// if (label != "") {
// toggle();
// onAdd();
// setSelectedState(!selected);
// }
// else {}
// }
// const type = label != "" ? "button" : "submit";
// useEffect(() => {
// setClassName("candidateButton " + (selected ? "candidateAdded" : ""))
// }, [selected]);
// useEffect(() => {
// setPlusIcon("mr-2 cursorPointer " + (selected ? "trashIcon" : ""))
// }, [selected]);
// useEffect(() => {
// setTrashIcon("trashIcon " + (selected ? "displayTrash" : ""))
// }, [selected]);
// const addFunction = () => {
// addCandidate();
// setSelectedState(!selected);
// }
// const removeCandidate = () => {
// onDelete();
// toggle();
// }
//const [image, setImage] = useState(null);
// const [createObjectURL, setCreateObjectURL] = useState(null);
// const uploadToClient = (event) => {
// if (event.target.files && event.target.files[0]) {
// const i = event.target.files[0];
// setImage(i);
// setCreateObjectURL(URL.createObjectURL(i));
// }
// };
return (<p>FOO</p>);
// return (
// <Row className="rowNoMargin">
// <div className={className}>
// <div className="avatarThumb">
// <img src={createObjectURL} alt="" />
// <input placeholder="Ajouter un candidat" className="candidate-placeholder ml-2" value={label} />
// </div>
//
// <FontAwesomeIcon onClick={toggle} icon={faPlus} className={plusIcon} />
// <div className={trashIcon}><TrashButton label={label} onDelete={onDelete} /></div>
// </div>
//
// <Modal
// isOpen={visibled}
// toggle={toggle}
// className="modal-dialog-centered"
// >
//
// <ModalHeader className='closeModalAddCandidate' toggle={toggle}>
//
// </ModalHeader>
// <ModalBody>
// <Col className="addCandidateCard">
// <InputGroup className="addCandidateForm">
// <Form>
// <InputGroupAddon addonType="prepend" className="addCandidateHeader">
// { //<DragHandle>
// }
// <h6>Ajouter un participant</h6>
// <p>Ajoutez une photo, le nom et une description au candidat.</p>
// <div className="ajout-avatar">
// <div>
// <div className="avatar-placeholer">
// <img src={createObjectURL} />
// </div>
// </div>
// <div className="avatar-text">
// <h4>Photo <span> (facultatif)</span></h4>
//
// <p>Importer une photo.<br />format : jpg, png, pdf</p>
// <div className="btn-ajout-avatar">
// <input type="file" name="myImage" id="myImage" value={avatar} onChange={uploadToClient} />
// <label className="inputfile" htmlFor="myImage">Importer une photo</label>
// </div>
// </div>
// </div>
// <img src="/avatar.svg" />
// { //</InputGroupAddon></DragHandle>
// }
// </InputGroupAddon>
// <Label className="addCandidateText">Nom et prenom</Label>
// <Input
// type="text"
// value={label}
// {...inputProps}
// placeholder={t("resource.candidatePlaceholder")}
// tabIndex={candIndex + 1}
// maxLength="250"
// autoFocus
// className="addCandidateText"
// required
// />
// <Label>Description (Facultatif)</Label>
// <Input
// type="text"
// defaultValue={description}
// maxLength="250"
// />
// <Row className="removeAddButtons">
//
// <ButtonWithConfirm className="removeButton" label={label} onDelete={removeCandidate} />
//
// <Button type={type} className="addButton" label={label} onClick={addCandidate}>
// <FontAwesomeIcon icon={faPlus} />
// <span>Ajouter</span>
// </Button>
//
// </Row>
// </Form>
// </InputGroup>
// </Col>
// </ModalBody></Modal>
// {/* <Col xs="auto" className="align-self-center pl-0">
// <HelpButton>
// {t(
// "Enter the name of your candidate or proposal here (250 characters max.)"
// )}
// </HelpButton>
// </Col> */}
// </Row >
// );
}
export default CandidateField

@ -1,136 +0,0 @@
import {useState, useEffect, createRef} from 'react'
import {useTranslation} from "react-i18next";
// import {DndContext, useDroppable} from '@dnd-kit/core';
import CandidateField from './CandidateField'
import Alert from '@components/Alert'
import {MAX_NUM_CANDIDATES} from '@services/constants';
// export function CandidateList(props) {
// const {isOver, setNodeRef} = useDroppable({
// id: props.id,
// });
// const style = {
// opacity: isOver ? 1 : 0.5,
// };
//
// return (
// <div ref={setNodeRef} style={style}>
// {props.children}
// </div>
// );
// }
// const SortableItem = sortableElement(({className, ...childProps}) => <li className={className}><CandidateField {...childProps} /></li>);
//
// const SortableContainer = sortableContainer(({children}) => {
// return <ul className="sortable">{children}</ul>;
// });
// const arrayMove = (arr, fromIndex, toIndex) => {
// // https://stackoverflow.com/a/6470794/4986615
// const element = arr[fromIndex];
// arr.splice(fromIndex, 1);
// arr.splice(toIndex, 0, element);
// return arr
// }
const CandidatesField = ({onChange}) => {
const {t} = useTranslation();
const createCandidate = () => ({label: "", description: "", fieldRef: createRef()})
// Initialize the list with at least two candidates
const [candidates, setCandidates] = useState([createCandidate(), createCandidate()])
const [error, setError] = useState(null)
const addCandidate = () => {
if (candidates.length < MAX_NUM_CANDIDATES) {
setCandidates(
c => {
c.push(createCandidate());
return c
}
);
} else {
setError('error.too-many-candidates')
}
};
// What to do when we change the candidates
useEffect(() => {
onChange();
}, [candidates])
const removeCandidate = index => {
if (candidates.length === 1) {
setCandidates([createCandidate()]);
}
else {
setCandidates(oldCandidates =>
oldCandidates.filter((_, i) => i != index)
);
}
};
const editCandidate = (index, label) => {
setCandidates(
oldCandidates => {
oldCandidates[index].label = label;
return oldCandidates;
}
)
};
const handleKeyPress = (e, index) => {
if (e.key !== "Enter") {
e.preventDefault();
if (index + 1 === candidates.length) {
addCandidate();
}
candidates[index + 1].fieldRef.current.focus();
}
}
const onSortEnd = ({oldIndex, newIndex}) => {
setCandidates(c => arrayMove(c, oldIndex, newIndex));
};
return (
<div className="sectionAjouterCandidat">
<div className="ajouterCandidat">
<h4>Saisissez ici le nom de vos candidats.</h4>
<Alert msg={error} />
{candidates.map((candidate, index) => {
const className = "sortable"
return (
<CandidateField
className={className}
key={`item-${index}`}
index={index}
candIndex={index}
label={candidate.label}
description={candidate.description}
onDelete={() => removeCandidate(index)}
onChange={(e) => editCandidate(index, e.target.value)}
onKeyPress={(e) => handleKeyPress(e, index)}
onAdd={addCandidate}
innerRef={candidate.fieldRef}
/>
)
})}
</div>
</div >
);
}
export default CandidatesField

@ -1,8 +1,6 @@
import Link from "next/link";
import {useTranslation} from "next-i18next";
import {Button, Row, Col} from "reactstrap";
import {useBbox} from "@components/layouts/useBbox";
import Paypal from "@components/banner/Paypal";
import Logo from '@components/Logo.jsx';
import LanguageSelector from "@components/layouts/LanguageSelector";
@ -57,7 +55,7 @@ const Footer = () => {
{
component: (
<a href="mailto:app@mieuxvoter.fr?subject=[HELP]" style={linkStyle}>
Nous contacter
{t('menu.contact-us')}
</a>
)
},
@ -84,7 +82,7 @@ const Footer = () => {
<Col className="col-auto">
<Button className="btn-info">
<a href="/">
Soutenez-nous
{t('common.support-us')}
</a>
</Button>
</Col>

9605
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -10,13 +10,11 @@
"export": "next export"
},
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@fortawesome/fontawesome-free": "^5.15.3",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.19",
"@svgr/webpack": "^6.2.1",
"babel-eslint": "^10.1.0",
"bootstrap": "^5.2.2",
"bootstrap-scss": "^5.2.2",

@ -24,10 +24,16 @@ function Application({Component, pageProps}) {
key="og:image"
/>
</Head>
<Header />
<main className="d-flex flex-column justify-content-center">
<Component {...pageProps} />
</main></AppProvider>);
<main className='d-flex flex-column justify-content-between'>
<div>
<Header />
<div className="d-flex flex-column justify-content-center">
<Component {...pageProps} />
</div>
</div>
<Footer />
</main>
</AppProvider>);
}

File diff suppressed because it is too large Load Diff

@ -35,10 +35,10 @@ import {serverSideTranslations} from "next-i18next/serverSideTranslations";
// import {useAppContext} from "@services/context";
// import {createElection} from "@services/api";
// import {translateGrades} from "@services/grades";
// import HelpButton from "@components/form/HelpButton";
// import HelpButton from "@components/admin/HelpButton";
// import Loader from "@components/wait";
// import CandidatesField from "@components/form/CandidatesField";
// import ConfirmModal from "@components/form/ConfirmModal";
// import CandidatesField from "@components/admin/CandidatesField";
// import ConfirmModal from "@components/admin/ConfirmModal";
// import config from "../../next-i18next.config.js";

@ -4,7 +4,6 @@ import Image from "next/image";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {useTranslation} from "next-i18next";
import {Container, Row, Col, Button, Input} from "reactstrap";
import Footer from '@components/layouts/Footer';
import Logo from '@components/Logo.jsx';
import {CREATE_ELECTION} from '@services/routes';
import ballotBox from '../public/urne.svg'
@ -203,7 +202,6 @@ const Home = () => {
<ExperienceRow />
<ShareRow />
</section>
<Footer />
</Container >
);

@ -1,8 +1,8 @@
import { useState, useEffect } from "react";
import {useState, useEffect} from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import {useRouter} from "next/router";
import {useTranslation} from "next-i18next";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {
Collapse,
Container,
@ -16,12 +16,12 @@ import {
Card,
CardBody,
} from "reactstrap";
import { ReactMultiEmail, isEmail } from "react-multi-email";
import {ReactMultiEmail, isEmail} from "react-multi-email";
import "react-multi-email/style.css";
import { toast, ToastContainer } from "react-toastify";
import {toast, ToastContainer} from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import queryString from "query-string";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faPlus,
faTrashAlt,
@ -29,13 +29,13 @@ import {
faCogs,
faExclamationTriangle,
} from "@fortawesome/free-solid-svg-icons";
import { useAppContext } from "@services/context";
import { createElection } from "@services/api";
import { translateGrades } from "@services/grades";
import HelpButton from "@components/form/HelpButton";
import {useAppContext} from "@services/context";
import {createElection} from "@services/api";
import {translateGrades} from "@services/grades";
import HelpButton from "@components/admin/HelpButton";
import Loader from "@components/wait";
import CandidatesField from "@components/form/CandidatesField";
import ConfirmModal from "@components/form/ConfirmModal";
import CandidatesField from "@components/admin/CandidatesField";
import ConfirmModal from "@components/admin/ConfirmModal";
import config from "../../next-i18next.config.js";
import Modal from '../../components/Modal'
@ -77,19 +77,19 @@ const displayClockOptions = () =>
</option>
));
export const getStaticProps = async ({ locale }) => ({
export const getStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const CreateElection = (props) => {
const { t } = useTranslation();
const {t} = useTranslation();
// default value : start at the last hour
const now = new Date();
const [title, setTitle] = useState("");
const [candidates, setCandidates] = useState([{ label: "" }, { description: "" }]);
const [candidates, setCandidates] = useState([{label: ""}, {description: ""}]);
const [numGrades, setNumGrades] = useState(5);
const [waiting, setWaiting] = useState(false);
const [isAdvancedOptionsOpen, setAdvancedOptionsOpen] = useState(false);
@ -109,7 +109,7 @@ const CreateElection = (props) => {
useEffect(() => {
if (!router.isReady) return;
const { title: urlTitle } = router.query;
const {title: urlTitle} = router.query;
setTitle(urlTitle || "");
}, [router.isReady]);
@ -131,14 +131,14 @@ const CreateElection = (props) => {
const addCandidate = () => {
if (candidates.length < 1000) {
candidates.push({ label: "" });
candidates.push({label: ""});
setCandidates(candidates);
}
};
const checkFields = () => {
if (!candidates) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
return {ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR};
}
let numCandidates = 0;
@ -146,14 +146,14 @@ const CreateElection = (props) => {
if (c.label !== "") numCandidates += 1;
});
if (numCandidates < 2) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
return {ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR};
}
if (!title || title === "") {
return { ok: false, msg: NO_TITLE_ERROR };
return {ok: false, msg: NO_TITLE_ERROR};
}
return { ok: true, msg: "OK" };
return {ok: true, msg: "OK"};
};
const handleSubmit = () => {
@ -231,15 +231,12 @@ const CreateElection = (props) => {
<Col className="stepFormCol">
<img src="/icone-three-dark.svg" />
<h4>Confirmation</h4>
</Col>
</Row>
@ -305,7 +302,10 @@ const CreateElection = (props) => {
<Row className="mt-4">
<Row className="mt-4">
<Col xs="12">
<CandidatesField onChange={setCandidates} />
</Col>
@ -428,7 +428,7 @@ const CreateElection = (props) => {
setStart(
new Date(
timeMinusDate(start) +
new Date(e.target.valueAsNumber).getTime()
new Date(e.target.valueAsNumber).getTime()
)
);
}}
@ -442,7 +442,7 @@ const CreateElection = (props) => {
setStart(
new Date(
dateMinusTime(start).getTime() +
e.target.value * 3600000
e.target.value * 3600000
)
)
}
@ -466,7 +466,7 @@ const CreateElection = (props) => {
setFinish(
new Date(
timeMinusDate(finish) +
new Date(e.target.valueAsNumber).getTime()
new Date(e.target.valueAsNumber).getTime()
)
);
}}
@ -480,7 +480,7 @@ const CreateElection = (props) => {
setFinish(
new Date(
dateMinusTime(finish).getTime() +
e.target.value * 3600000
e.target.value * 3600000
)
)
}

@ -1,36 +1,35 @@
import { useState, useCallback, useEffect } from "react";
import {useState, useCallback, useEffect} from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";
import { Button, Col, Container, Row, Modal, ModalHeader, ModalBody, } from "reactstrap";
import {useRouter} from "next/router";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {useTranslation} from "next-i18next";
import {Button, Col, Container, Row, Modal, ModalHeader, ModalBody, } from "reactstrap";
import Link from "next/link";
import { toast, ToastContainer } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { getDetails, castBallot, apiErrors } from "@services/api";
// import {toast, ToastContainer} from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCheck} from "@fortawesome/free-solid-svg-icons";
import {getDetails, castBallot, apiErrors} from "@services/api";
import Error from "@components/Error";
import { translateGrades } from "@services/grades";
import config from "../../../next-i18next.config.js";
import {translateGrades} from "@services/grades";
import Footer from '@components/layouts/Footer'
import useEmblaCarousel from 'embla-carousel-react'
import { DotButton, PrevButton, NextButton } from "../../../components/form/EmblaCarouselButtons";
import VoteButtonWithConfirm from "../../../components/form/VoteButtonWithConfirm";
// import useEmblaCarousel from 'embla-carousel-react'
import {DotButton, PrevButton, NextButton} from "@components/admin/EmblaCarouselButtons";
import VoteButtonWithConfirm from "@components/admin/VoteButtonWithConfirm";
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({ query: { pid, tid }, locale }) {
export async function getServerSideProps({query: {pid, tid}, locale}) {
const [details, translations] = await Promise.all([
getDetails(pid),
serverSideTranslations(locale, [], config),
serverSideTranslations(locale, ['resource']),
]);
if (typeof details === "string" || details instanceof String) {
return { props: { err: details, ...translations } };
return {props: {err: details, ...translations}};
}
if (!details.candidates || !Array.isArray(details.candidates)) {
return { props: { err: "Unknown error", ...translations } };
return {props: {err: "Unknown error", ...translations}};
}
shuffle(details.candidates);
@ -40,7 +39,7 @@ export async function getServerSideProps({ query: { pid, tid }, locale }) {
...translations,
invitationOnly: details.on_invitation_only,
restrictResults: details.restrict_results,
candidates: details.candidates.map((name, i, infos) => ({ id: i, label: name, description: infos })),
candidates: details.candidates.map((name, i, infos) => ({id: i, label: name, description: infos})),
title: details.title,
numGrades: details.num_grades,
pid: pid,
@ -49,8 +48,8 @@ export async function getServerSideProps({ query: { pid, tid }, locale }) {
};
}
const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
const { t } = useTranslation();
const VoteBallot = ({candidates, title, numGrades, pid, err, token}) => {
const {t} = useTranslation();
if (err) {
return <Error value={apiErrors(err, t)}></Error>;
@ -111,7 +110,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
const toggleDesktop = () => setVisibilityDesktop(!visibledDesktop)
const [visibledDesktop, setVisibilityDesktop] = useState(false);
const [viewportRef, embla] = useEmblaCarousel({ skipSnaps: false });
const [viewportRef, embla] = useEmblaCarousel({skipSnaps: false});
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
@ -323,7 +322,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: "#fff"}
: {
backgroundColor: "transparent",
color: "#000",
@ -362,7 +361,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: "#fff"}
: {
backgroundColor: "#C3BFD8",
color: "#000",
@ -371,7 +370,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
>
<small
className="nowrap bold badge"
style={{ backgroundColor: "transparent", color: "#fff" }}
style={{backgroundColor: "transparent", color: "#fff"}}
>
{grade.label}
</small>
@ -460,7 +459,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: "#fff"}
: {
backgroundColor: "transparent",
color: "#000",
@ -499,7 +498,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: "#fff"}
: {
backgroundColor: "#C3BFD8",
color: "#000",
@ -508,7 +507,7 @@ const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
>
<small
className="nowrap bold badge"
style={{ backgroundColor: "transparent", color: "#fff" }}
style={{backgroundColor: "transparent", color: "#fff"}}
>
{grade.label}
</small>

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 11.5H4M12 4V20" stroke="white" stroke-width="2" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 11H20V13H19V11ZM5 12L4.29289 12.7071L3.58579 12L4.29289 11.2929L5 12ZM11.2929 4.29289L12 3.58579L13.4142 5L12.7071 5.70711L11.2929 4.29289ZM12.7071 18.2929L13.4142 19L12 20.4142L11.2929 19.7071L12.7071 18.2929ZM19 13H5V11H19V13ZM12.7071 5.70711L5.70711 12.7071L4.29289 11.2929L11.2929 4.29289L12.7071 5.70711ZM5.70711 11.2929L12.7071 18.2929L11.2929 19.7071L4.29289 12.7071L5.70711 11.2929Z" fill="#0A004C"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

@ -1,4 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="white" fill-opacity="0.16"/>
<path d="M11.9998 15.333C14.4412 15.333 16.5765 16.383 17.7378 17.9497L16.5098 18.5303C15.5645 17.4103 13.8978 16.6663 11.9998 16.6663C10.1018 16.6663 8.43518 17.4103 7.48985 18.5303L6.26251 17.949C7.42385 16.3823 9.55851 15.333 11.9998 15.333ZM11.9998 5.33301C12.8839 5.33301 13.7317 5.6842 14.3569 6.30932C14.982 6.93444 15.3332 7.78229 15.3332 8.66634V10.6663C15.3331 11.5253 15.0015 12.3511 14.4074 12.9715C13.8133 13.5919 13.0027 13.9591 12.1445 13.9963L11.9998 13.9997C11.1158 13.9997 10.2679 13.6485 9.64282 13.0234C9.0177 12.3982 8.66651 11.5504 8.66651 10.6663V8.66634C8.66656 7.80737 8.99821 6.98157 9.59229 6.36116C10.1864 5.74075 10.997 5.37362 11.8552 5.33634L11.9998 5.33301ZM11.9998 6.66634C11.4897 6.66631 10.9988 6.86122 10.6277 7.2112C10.2565 7.56117 10.0331 8.03975 10.0032 8.54901L9.99985 8.66634V10.6663C9.99934 11.1869 10.2019 11.6872 10.5644 12.0609C10.9269 12.4345 11.4208 12.6521 11.9412 12.6674C12.4615 12.6827 12.9674 12.4945 13.3512 12.1427C13.735 11.791 13.9665 11.3034 13.9965 10.7837L13.9998 10.6663V8.66634C13.9998 8.13591 13.7891 7.6272 13.4141 7.25213C13.039 6.87705 12.5303 6.66634 11.9998 6.66634Z" fill="white"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99978 11.333C10.4411 11.333 12.5765 12.383 13.7378 13.9497L12.5098 14.5303C11.5645 13.4103 9.89778 12.6663 7.99978 12.6663C6.10178 12.6663 4.43512 13.4103 3.48978 14.5303L2.26245 13.949C3.42378 12.3823 5.55845 11.333 7.99978 11.333ZM7.99978 1.33301C8.88384 1.33301 9.73169 1.6842 10.3568 2.30932C10.9819 2.93444 11.3331 3.78229 11.3331 4.66634V6.66634C11.3331 7.52532 11.0014 8.35112 10.4073 8.97153C9.81327 9.59194 9.00262 9.95906 8.14445 9.99634L7.99978 9.99967C7.11573 9.99967 6.26788 9.64849 5.64276 9.02336C5.01764 8.39824 4.66645 7.5504 4.66645 6.66634V4.66634C4.6665 3.80737 4.99815 2.98157 5.59222 2.36116C6.1863 1.74075 6.99695 1.37362 7.85512 1.33634L7.99978 1.33301ZM7.99978 2.66634C7.48964 2.66631 6.99877 2.86122 6.62761 3.2112C6.25645 3.56117 6.03305 4.03975 6.00312 4.54901L5.99978 4.66634V6.66634C5.99928 7.18694 6.2018 7.68723 6.5643 8.06089C6.9268 8.43455 7.42071 8.65213 7.94109 8.66741C8.46147 8.68268 8.9673 8.49445 9.3511 8.1427C9.7349 7.79095 9.96641 7.30341 9.99645 6.78367L9.99978 6.66634V4.66634C9.99978 4.13591 9.78907 3.6272 9.414 3.25213C9.03893 2.87705 8.53022 2.66634 7.99978 2.66634Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -26,7 +26,13 @@
"menu.whoarewe": "Who are we?",
"menu.faq": "FAQ",
"menu.news": "News",
"menu.contact-us": "Contact us",
"common.save": "Save",
"common.back-homepage": "Return to home page",
"common.support-us": "Support us",
"common.thumbnail": "Thumbnail",
"common.name": "Name",
"common.cancel": "Cancel",
"error.help": "Ask for our help",
"error.at-least-2-candidates": "At least two candidates are required.",
"error.no-title": "Please add a title to your election.",
@ -35,6 +41,19 @@
"grades.passable": "Passable",
"grades.inadequate": "Inadequate",
"grades.mediocre": "Mediocre",
"admin.date-limit": "Set a deadline for voting",
"admin.step-candidate": "Candidates",
"admin.step-params": "Parameters",
"admin.step-confirm": "Confirm",
"admin.add-candidates": "Add the candidates.",
"admin.add-candidate": "Add a candidate.",
"admin.add-candidate-desc": "Add a picture, a name, and a description of the candidate.",
"admin.candidate-name-placeholer": "Add the name or the title of the candidate.",
"admin.candidate-desc-placeholer": "Add the description of the candidate.",
"admin.photo": "Picture",
"admin.optional": "optional",
"admin.photo-import": "Import a picture",
"admin.photo-type": "Supported type:",
"Homepage": "Homepage",
"Source code": "Source code",
"Who are we?": "Who are we?",

@ -26,10 +26,35 @@
"menu.whoarewe": "Qui sommes-nous ?",
"menu.faq": "FAQ",
"menu.news": "Actualités",
"common.backHomepage": "Revenir sur la page d'accueil",
"menu.contact-us": "Nous contacter",
"common.back-homepage": "Revenir sur la page d'accueil",
"common.support-us": "Soutenez nous",
"common.save": "Sauvegarder",
"common.thumbnail": "Image miniature",
"common.name": "Nom",
"common.description": "Description",
"common.cancel": "Annuler",
"error.help": "Besoin d'aide ?",
"error.at-least-2-candidates": "Ajoutez au moins deux candidats.",
"error.no-title": "Ajoutez un titre à l'élection.",
"grades.very-good": "Très bien",
"grades.good": "Bien",
"grades.passable": "Passable",
"grades.inadequate": "Insuffisant",
"grades.mediocre": "Médiocre",
"admin.date-limit": "Fixer une date limite pour le vote",
"admin.step-candidate": "Les candidats",
"admin.step-params": "Paramètres du vote",
"admin.step-confirm": "Confirmation",
"admin.add-candidates": "Ajouter les candidats.",
"admin.add-candidate": "Ajouter un candidat.",
"admin.candidate-name-placeholer": "Ajouter le nom ou le titre du candidat.",
"admin.candidate-desc-placeholer": "Ajouter la description du candidat.",
"admin.add-candidate-desc": "Ajouter une photo, le nom et une description au candidat.",
"admin.photo": "Photo",
"admin.optional": "facultatif",
"admin.photo-import": "Importer une photo",
"admin.photo-type": "Format supporté :",
"Homepage": "Accueil",
"Source code": "Code source",
"Who are we?": "Qui sommes-nous ?",
@ -115,7 +140,6 @@
"You need a token to vote in this election": "Vous avez besoin d'un jeton pour participer à ce vote",
"You seem to have already voted.": "Il semble que vous ayez déjà voté.",
"The parameters of the election are incorrect.": "Les paramètres de vote sont incorrects.",
"Support us !": "Soutenez-nous !",
"PayPal - The safer, easier way to pay online!": "PayPal - Le moyen le plus sûr et le plus simple de payer en ligne !",
"resource.numVotes": "Nombre de votes :",
"Access to results": "Accès aux résultats",

@ -2,6 +2,6 @@
* This file provides useful constants for the project
*/
const MAX_NUM_CANDIDATES = process.env.MAX_NUM_CANDIDATES || 1000;
const CONTACT_MAIL = process.env.CONTACT_MAIL || "app@mieuxvoter.fr";
const DEFAULT_GRADES = process.env.DEFAULT_GRADES || ['grades.very-good', 'grades.good', 'grades.passable', 'grades.inadequate', 'grades.mediocre'];
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'];

@ -2,4 +2,4 @@
* This file provides the paths to the pages
*/
const CREATE_ELECTION = '/admin/new/';
export const CREATE_ELECTION = '/admin/new/';

@ -0,0 +1,37 @@
.creation-step {
color: rgb(255,255,255,0.64);
.name {
font-weight: 500;
font-size: 16px;
}
.badge {
width: 24px;
height: 24px;
background-color: rgb(0,0,0, 0.2);
}
}
.creation-step.active {
.badge {
background-color: white;
color: $mv-blue-color;
}
.name {
color: white;
}
}
.candidate, .creation-steps {
max-width: 500px;
}
.default-avatar {
background-color: rgba(255, 255, 255, 0.16);
width: inherit!important;
position: inherit!important;
padding: 4px;
margin-right: 10px;
}

@ -57,9 +57,15 @@ $theme-colors: (
}
html,
body,
#__next,
#__next > div {
height: 100%;
main {
min-height: 100vh;
}
html,
body,
main,
main > .div {
width: 100%;
}
main {
@ -316,25 +322,6 @@ h5 {
font-size: 18px;
font-weight: bold;
}
.btn {
//width: 165px;
padding: 8px 24px;
// background: $mv-blue-color;
border: 2px solid #ffffff;
border-radius: 0px;
box-sizing: border-box;
box-shadow: 0px 4px 0px #ffffff;
font-family: DM Sans;
font-style: normal;
font-weight: bold;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.5px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
}
/*.btn img {
width: 24px !important;
height: 24px !important;
@ -404,3 +391,8 @@ h5 {
ol.result > li {
text-align: center;
}
.border-dashed {
border-style: dashed!important;
}

@ -1,3 +1,22 @@
.btn {
//width: 165px;
padding: 8px 24px;
// background: $mv-blue-color;
border-width: 2px;
border-style: solid;
border-radius: 0px;
box-sizing: border-box;
box-shadow: 0px 4px 0px;
font-family: DM Sans;
font-style: normal;
font-weight: bold;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.5px;
display: flex;
justify-content: center;
align-items: center;
}
.btn-opacity:hover {
background-color: transparent;
border-color: white;
@ -22,6 +41,8 @@
.btn-secondary {
width: fit-content;
background-color: transparent;
border-color: white;
color: white;
}
.btn-primary {
box-sizing: border-box;
@ -39,7 +60,6 @@
box-shadow: 0px 5px 0px #7a64f9;
}
.btn:hover {
background-color: rgb(255,255,255,0.2);
border-color: white;
background-color: rgb(255, 255, 255, 0.2);
border-color: white;
}

@ -0,0 +1,31 @@
.modal .overlay {
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
}
.modal .body {
background: white;
width: 500px;
height: 600px;
border-radius: 15px;
padding: 15px;
}
.modal .body {
display: flex;
justify-content: flex-end;
font-size: 25px;
}
.modal {
border-radius: unset;
}
h4.modal-title {
color: $mv-blue-color;
}

@ -9,6 +9,9 @@ $mv-dark-color: #333;
$body-bg: #000000;
$body-color: $mv-light-color;
$modal-content-border-radius: 0px;
$modal-header-border-width: 0px;
$theme-colors: (
"primary": $mv-blue-color,
"secondary": $mv-dark-blue-color,
@ -22,10 +25,12 @@ $theme-colors: (
@import "_bootstrap.scss";
@import "app.scss";
@import "_buttons.scss";
@import "_header.scss";
@import "_footer.scss";
@import "_modal.scss";
@import "_homePage.scss";
@import "_vote.scss";
@import "_newVote.scss";
@import "_resultVote.scss";
@import "_header.scss";
@import "_vote.scss";
@import "_buttons.scss";
@import "_footer.scss";
@import "_admin.scss";

Loading…
Cancel
Save