UNPKG

@proca/widget

Version:

Proca is an open-source campaign toolkit designed to empower activists and organisations in their digital advocacy efforts. It provides a flexible and customisable platform for creating and managing online petitions, email campaigns, and other forms of di

424 lines (394 loc) 12.4 kB
import React, { useEffect, useState, useRef } from "react"; import PropTypes from "prop-types"; import { Container, Grid } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; import { TextField as MUITextField, Button, Snackbar } from "@material-ui/core"; import TextField from "@components/TextField"; import Alert from "@material-ui/lab/Alert"; import SendIcon from "@material-ui/icons/Send"; import DoneIcon from "@material-ui/icons/Done"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import ProgressCounter from "@components/ProgressCounter"; import Birthdate from "@components/field/Birthdate"; import { checkMail, getDomain } from "@lib/checkMail"; import { addActionContact } from "@lib/server.js"; import useElementWidth from "@hooks/useElementWidth"; import { useConfig } from "@hooks/useConfig"; import useData from "@hooks/useData"; import uuid from "@lib/uuid.js"; import domparser from "@lib/domparser"; import { useInitFromUrl } from "@hooks/useCount"; import { url as postcardUrl } from "./Download"; let defaultValues = { firstname: "", lastname: "", email: "", postcode: "", locality: "", address: "", country: "CH", comment: "", }; const useStyles = makeStyles(theme => ({ container: { display: "flex", flexWrap: "wrap", }, textField: { marginLeft: theme.spacing(0), marginRight: theme.spacing(0), width: "100%", }, "#petition-form": { position: "relative" }, "@global": { "select:-moz-focusring": { color: "transparent", textShadow: "0 0 0 #000", }, "input:valid + fieldset": { borderColor: "green", borderWidth: 2, }, }, })); export default function Register(props) { const classes = useStyles(); const emailProvider = useRef(undefined); // we don't know the email provider const { t } = useTranslation(); const [status, setStatus] = useState("init"); const [config, setCampaignConfig] = useConfig(); const [data, setData] = useData(); const actionUrl = config.component.initiative.prefixActionPage + domparser("campaignId", config.selector); const [c, actionPage] = useInitFromUrl(actionUrl); if (status !== "error" && c && c.errors && c.errors.length >= 0) { setStatus("error"); } const buttonRegister = config.buttonRegister || t("action.sign"); useEffect(() => { if (!actionPage || actionPage === config.actionPage) return; setCampaignConfig(config => { const d = JSON.parse(JSON.stringify(config)); d.actionPage = actionPage; setStatus("default"); return d; }); }, [actionPage, setCampaignConfig, setStatus, config.actionPage]); defaultValues = { ...defaultValues, ...config.data }; const width = useElementWidth("#proca-register"); const [compact, setCompact] = useState(true); if ((compact && width > 450) || (!compact && width <= 450)) setCompact(width <= 450); const form = useForm({ mode: "onBlur", defaultValues: { ...defaultValues, ...data }, }); const { register, handleSubmit, setValue, formState: { errors }, setError, clearErrors, watch, formState, } = form; const postcode = watch("postcode"); const locality = watch("locality"); const [autoLocality, setLocality] = useState(""); const [region, setRegion] = useState(""); useEffect(() => { if (postcode.length !== 4) return; const api = `https://postcode-ch.proca.foundation/${postcode}`; async function fetchAPI() { await fetch(api) .then(res => { if (!res.ok) { throw Error(res.statusText); } return res.json(); }) .then(res => { if (res && res.name) { setLocality(res.name); setRegion(res.code1); setValue("locality", res.name); } }) .catch(() => { // no error for now /* setError("postcode", { type: "network", message: (err && err.toString()) || "Network error", }); */ }); } fetchAPI(); }, [postcode, setError, setValue]); const options = { margin: config.margin || "dense", variant: config.variant || "filled", }; //variant: standard, filled, outlined //margin: normal, dense const validateEmail = async email => { if (config.component?.register?.validateEmail === false) return true; if (emailProvider.current) return true; // might cause some missing validation on edge cases const provider = await checkMail(email); emailProvider.current = provider; if (provider === false) { return t("email.invalid_domain", { defaultValue: "{{domain}} cannot receive emails", domain: getDomain(email), }); } return true; }; const onSubmit = async data => { data.tracking = config.utm; data.region = region; data.country = "CH"; data.LanguageCode = config.lang; data.birthdate = formatDate(data.birthdate); if (data.birthdate === false) { setError( "birthdate", "manual", t("error.date", "format should be DD.MM.YYYY") ); return; } data.postcardUrl = postcardUrl(data, config.param); if (config.component.consent?.implicit) { data.privacy = config.component.consent.implicit === true ? null : config.component.consent.implicit; // implicit true or opt-in or opt-out } const result = await addActionContact("register", config.actionpage, data); if (result.errors) { let handled = false; if (result.errors.fields) { result.errors.fields.forEach(field => { if (field.name in data) { setError(field.name, { type: "server", message: field.message }); handled = true; } else if (field.name.toLowerCase() in data) { setError(field.name.toLowerCase(), { type: "server", message: field.message, }); handled = true; } }); } !handled && setStatus("error"); return; } setStatus("success"); delete data.tracking; delete data.privacy; uuid(result.contactRef); // set the global uuid as signature's fingerprint data.uuid = uuid(); if (!config.actionpage) { setStatus("error"); console.log( `Attempt to create QRCode with actionPage id = ${config.actionpage}` ); } data.postcardUrl += `&qrcode=${uuid()}:${config.actionpage}`; setData(data); if (props.done instanceof Function) props.done(data); // sends the signature's ID as fingerprint }; useEffect(() => { const inputs = document.querySelectorAll("input, select, textarea"); // todo: workaround until the feature is native react-form ? inputs.forEach(input => { input.oninvalid = e => { setError(e.target.attributes.name.nodeValue, { type: e.type, message: e.target.validationMessage, }); }; }); }, [register, setError]); const handleBlur = e => { e.target.checkValidity(); if (e.target.validity.valid) { clearErrors(e.target.attributes.name.nodeValue); return; } }; function formatDate(date) { if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(date)) return date; if (date.length > 0) { if (date.length !== 10) { return false; } const dmj = date.split(/ |\.|\//); if (dmj.length !== 3) return false; return `${dmj[2]}-${dmj[1]}-${dmj[0]}`; } } function minBirthdate() { const d = new Date(); d.setFullYear(d.getFullYear() - 18); return d; // return d.toISOString().substr(0, 10); } function ErrorS(props) { if (props.display) return ( <Snackbar open={true} autoHideDuration={6000}> <Alert severity="error"> Sorry, we could not save your signature! <br /> The techies have been informed. </Alert> </Snackbar> ); return null; } function Success(props) { if (props.display) return ( <Snackbar open={true} autoHideDuration={6000}> <Alert severity="success">Done, Thank you for your support!</Alert> </Snackbar> ); return null; } if (status === "success") { return ( <Container component="main" maxWidth="sm"> <Grid container spacing={1}> <Grid item xs={12}> <DoneIcon color="action" fontSize="large" my={4} /> </Grid> </Grid> </Container> ); } return ( <React.Fragment> <ProgressCounter actionPage={null} count={c && c.total} /> <form className={classes.container} onSubmit={handleSubmit(onSubmit)} method="post" id="proca-register" action="http://localhost" > <Success display={status === "success"} /> <ErrorS display={status === "error"} /> <Container component="main" maxWidth="sm"> <Grid container spacing={1}> <Grid item xs={12} sm={compact ? 12 : 6}> <TextField form={form} name="firstname" label={t("First name")} placeholder="eg. Albert" autoComplete="given-name" required /> </Grid> <Grid item xs={12} sm={compact ? 12 : 6}> <TextField form={form} name="lastname" label={t("Last name")} autoComplete="family-name" className={classes.textField} placeholder="eg. Einstein" required /> </Grid> <Grid item xs={12} sm={compact ? 12 : 6}> <TextField form={form} name="email" validate={validateEmail} type="email" label={t("Email")} autoComplete="email" required /> </Grid> <Grid item xs={12} sm={compact ? 12 : 6}> <Birthdate form={form} min={minBirthdate()} /> </Grid> <Grid item xs={12} sm={compact ? 12 : 12}> <TextField form={form} name="address" label={t("Address")} required /> </Grid> <Grid item xs={12} sm={compact ? 12 : 3}> <MUITextField id="postcode" name="postcode" required label={t("Postal Code")} onBlur={handleBlur} inputProps={{ pattern: "[0-9]{4}", title: "9999" }} autoComplete="postal-code" error={!!errors.postcode} helperText={ errors && errors.postcode && errors.postcode.message } {...register("postcode")} className={classes.textField} variant={options.variant} margin={options.margin} /> </Grid> <Grid item xs={12} sm={compact ? 12 : 9}> <TextField form={form} name="locality" label={t("Locality")} autoComplete="address-level2" InputLabelProps={{ shrink: autoLocality !== "" || locality !== "" || false, }} /> </Grid> <Grid item xs={12}> <Button color="primary" variant="contained" fullWidth type="submit" size="large" disabled={formState.isSubmitting} endIcon={<SendIcon />} > {" "} {buttonRegister} </Button> <input type="hidden" {...register("privacy", { required: false })} /> </Grid> </Grid> </Container> </form> </React.Fragment> ); } Register.propTypes = { actionPage: PropTypes.number.isRequired, }; Register.defaultProps = { buttonText: "Register", };