diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a53ec..594867c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,40 @@ # Changelog +### 1.9.0 + +1. Nueva funcion: Apoyar un proyecto. Ahora se puede apoyar un proyecto, como usuario registrado o como anonimo (con la necesidad de ingresar la informacion, un captcha, y una validacion por email) +2. Se agrega la posibilida de que en el perfil del diputada/o se pueda descargar la planilla con un listado de la informacion de quienes apoyaron los proyectos +3. Cambios visuales: Ahora los header de las cards de los proyectos tendrán una imagen de -LA PRIMERA- etiqueta/categoria elegida en el formulario del proyecto. En el caso de que no existiera, se pondrá una imagen estandar + +Listado de cambios: + +- Agregado campo "apoyos" en Documents como array de strings (emails) +- Agregado campo "apoyosCount" en las apis que devuelven Documents (para mostrar en tarjetas de home y dentro del proyecto) +- Se implementó un método de captcha para apoyos externos +- Se desarrollo un circuito de validación de apoyo externo (usuarix no registradx) por email usando tabla de tokens +- Al apoyar, se valida que ese mail no haya apoyado ya una vez +- Al apoyar, se valida que ese mail no tenga un token de validación vigente (creado en las últimas 48hs) +- Verificacion del captcha! (svg-captcha https://www.npmjs.com/package/svg-captcha) +- Usuarix no registradxs: Al poner mail y nombre se le avisa a lx usuarix de que va a recibir un mail para validar +- Se genera token de un solo uso (que "caduque" a las 48hs) para validar voto +- Se crea la tabla apoyoTokens con campos: token, fecha creación, email +- El token se generará como uuid v4 (https://www.npmjs.com/package/uuid) +- En el script init borraran los token más viejos que de 48hs +- Enviar mail (desde notifier) con link con el token en la url para validar el voto +- Cuando alguien X entré a validar el token, se verifica que este en el rango de 48hs (sino se avisa que ya expiró), y si lo está se registra un apoyo a nombre del mail del token y se borra el token +- Para apoyos internos (usuarix registradx) simplemente registrar el apoyo y ya +- Se agrega la posibilidad de descargar un excel con todos los apoyos registrados en los proyectos. +- Se quito el campo de URL de la imagen, dado que no va a ser mas utlilzado +- Ahora las etiquetas cuentan con una "key", importante para coordinar con que imagen mostrar en el header de la card del proyecto +- Ahora para monitores mas grandes-largos (2K/4K) se muestran 4 columnas de cards de proyectos en la home + +Compatible con: +* `leyesabiertas-web:1.9.0` +* `leyesabiertas-core:1.9.0` +* `leyesabiertas-notifier:1.9.0` +* `leyesabiertas-keycloak:1.8.0` + ### 1.8.1 * Cambiado la extension de excel de xls a xlsx @@ -10,7 +44,6 @@ Compatible con: * `leyesabiertas-notifier:1.8.0` * `leyesabiertas-keycloak:1.8.0` - ### 1.8.0 Listado de cambios hasta el momento: diff --git a/components/apoyar-formulario/component.js b/components/apoyar-formulario/component.js new file mode 100644 index 0000000..7ff8305 --- /dev/null +++ b/components/apoyar-formulario/component.js @@ -0,0 +1,265 @@ +import React, { Component, Fragment } from 'react' +import styled from 'styled-components' +import fetch from 'isomorphic-unfetch' +import WithUserContext from '../../components/with-user-context/component' +import getConfig from 'next/config' +const { publicRuntimeConfig: { API_URL } } = getConfig() + +const Container = styled.form` + z-index: 2 + position: absolute + width: 330px + right: 0 + background-color: white + padding: 1.2em + box-shadow: 0px 2px 4px #cac7c7 + color: black + text-align: left + text-transform: none + cursor: auto; + margin-top: 7px; + font-size:1.7rem + + @media(max-width:700px){ + position: fixed; + bottom: 0; + left: 0; + width: 100%; + } + + input { + padding: 5px; + } +` + +const Label = styled.label` + display: block + span { + display: block + font-weight: bold + padding: 14px 0px 7px + } + input { + width: 100% + } +` + +const ApoyosSpan = styled.span` + color: #6f78e6 + font-weight: bold +` + +const ApoyarButton = styled.button` + width: 100% + padding: 13px 0; + background-color: #6f78e6 + color: white + font-weight: bold + border: none + :focus {outline:0;} + display: inline-flex; + align-items: center; + justify-content: center; + + img{ + position: relative; + top: 1px; + margin-right: 5px; + } +` +const CloseButton = styled.div` + width: 65px; + margin-left: auto; + margin-bottom: 7px; + background-color: transparent; + border: none; + color: #960c0c; + cursor: pointer; + font-weight: bold; + font-size: 1.3rem; + @media(max-width:700px){ + font-size: 1.7rem; + } +` + +const CaptchaGroup = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding-top: 20px; + img { + padding-bottom: 5px; + } + input { + width: 70px; + text-align: center; + /*text-transform: uppercase;*/ + } +` + +const CaptchaTitle = styled.span` + font-weight: bold; +` + +const ErrorSpan = styled.span` + display: block; + color:red + padding-bottom: 5px; + margin-top: 20px; +` + +const ApoyandoGroup = styled.div` + text-align: center + display: flex; + flex-direction: column; + img { + margin-bottom: 18px; + height: 40px; + } +` + +const ApoyandoSpan = styled.span` + font-weight: bold; + margin-bottom: 8px; +` + +const ApoyandoPersonasSpan = styled.span` + color: #6f78e6 + font-weight: bold; +` + +class ApoyarFormulario extends Component { + state = { + formError: null, + svg: null, + token: null, + nombre_apellido: '', + email: '', + captcha: '', + } + + constructor (props) { + super(props) + + this.nombreApellidoInput = this.nombreApellidoInput.bind(this) + this.emailInput = this.emailInput.bind(this) + this.captchaInput = this.captchaInput.bind(this) + this.closeClick = this.closeClick.bind(this) + + this.handleSubmit = this.handleSubmit.bind(this) + } + + nombreApellidoInput(e) { this.setState({ nombre_apellido: e.target.value }) } + emailInput(e) { this.setState({ email: e.target.value }) } + captchaInput(e) { this.setState({ captcha: e.target.value }) } + + handleSubmit(e){ + e.preventDefault() + + const { authenticated } = this.props.authContext + + this.props.apoyarProyecto(!authenticated && { + token: this.state.token, + nombre_apellido: this.state.nombre_apellido, + email: this.state.email, + captcha: this.state.captcha, + }).then(async (res) => { + if (res.status == 200){ + this.setState({formError: null}) + if (!authenticated) + this.props.apoyoAnonExitoso() + }else{ + let err + try { + err = (await res.json()).error + } catch(_) { + err = "Ha ocurrido un error" + } + this.setState({formError: err}) + } + }) + } + + closeClick(){ + const hideApoyarForm = localStorage.getItem('hide-apoyar-form') || false + if (!hideApoyarForm) + localStorage.setItem('hide-apoyar-form', true) + this.props.toggleFormulario() + } + + componentWillMount() { + if (!this.props.authContext.authenticated && !this.state.svg) { + fetch(`${API_URL}/api/v1/documents/captcha-data`) + .then(r => r.json()) + .then(j => this.setState({svg: j.img, token: j.token})) + } + } + + render () { + const { authenticated, user } = this.props.authContext + const { project, hasAnonApoyado } = this.props + const { svg } = this.state + + if (!project) return null + + const apoyosMinusOne = project.apoyosCount-1 + + return ( + + CERRAR ✖ + { project.userIsApoyado && + + + ¡Ya estás apoyando esta propuesta! + {project.apoyosCount > 1 && + + { apoyosMinusOne } {apoyosMinusOne == 1 ? 'persona' : 'personas'} y vos + Están apoyando la propuesta + + } + + } + { hasAnonApoyado && + + + ¡Gracias! + + Enviamos un mail a su casilla para validar su apoyo + + + } + { !hasAnonApoyado && !project.userIsApoyado && + + { project.apoyosCount || 0 } personas están apoyando la propuesta
+ ¿Querés apoyarla también? + { !authenticated && + + + + + Validá que no sos un robot: + {svg ? +
+ : + Cargando imagen... + } + + + + } + {this.state.formError} + Quiero apoyar la propuesta + + } + + ) + } +} + +export default WithUserContext(ApoyarFormulario) diff --git a/components/card/component.js b/components/card/component.js index 0555a2e..a48f917 100644 --- a/components/card/component.js +++ b/components/card/component.js @@ -5,10 +5,11 @@ import styled from 'styled-components' import CardHeader from '../../elements/card-header/component' import CardContent from '../../elements/card-content/component' import CardSocial from '../../elements/card-social/component' +import WithDocumentTagsContext from '../../components/document-tags-context/component' const CardContainer = styled.div` margin: 0 1% 30px; -width: 31%; +width: 23%; box-shadow: 0 4px 20px 0 rgba(0,0,0,0.05); background-color: #ffffff; border: solid 1px #e9e9e9; @@ -17,28 +18,34 @@ box-sizing: border-box; cursor: pointer; display: block; position: relative; -@media (max-width: 1100px) { +@media (max-width: 1408px) { + width: 31%; + } +@media (max-width: 1216px) { width: 48%; } -@media (max-width: 760px) { +@media (max-width: 600px) { width: 100%; } ` -const Card = ({ project }) => ( +const Card = ({ project, tags }) => ( - + {/* */} + 0} img={`/static/assets/images/${tags && project.currentVersion.content.tags && project.currentVersion.content.tags.length > 0 ? tags.find(x => project.currentVersion.content.tags[0] == x.value).key : 'trama-default'}.jpg`} published={project.published} /> 0} party={project.author.fields && project.author.fields.party ? project.author.fields.party : ''} /> + closed={project.closed} + apoyosCount={project.apoyosCount} /> @@ -48,4 +55,4 @@ Card.propTypes = { project: PropTypes.object.isRequired } -export default Card +export default WithDocumentTagsContext(Card) diff --git a/components/project-fields/component.js b/components/project-fields/component.js index c03da96..b1787f7 100644 --- a/components/project-fields/component.js +++ b/components/project-fields/component.js @@ -982,14 +982,14 @@ class ProjectFields extends Component { onChange={this.handleInputChange} placeholder='Hacer uso correcto de mayúsculas y minúsculas' /> - + {/* Ingrese la URL para la imagen de encabezado: - + */} Fecha de cierre del proyecto: {/* ( +const ProjectHeader = ({ project, section, isPublished, isAuthor, setPublish, togglePublish, contextualCommentsCount, contributionsCount, contributorsCount, currentSection, withComments, apoyarProyecto }) => ( - + // + Presentación Artículos + } {currentSection === '/versiones' && Presentación Artículos + } {currentSection === '/articulado' && Presentación Artículos + {withComments ? : }  Modo lectura diff --git a/components/project-table-item/component.js b/components/project-table-item/component.js index 817af25..9d57207 100644 --- a/components/project-table-item/component.js +++ b/components/project-table-item/component.js @@ -94,7 +94,8 @@ export default ({ project }) => ( {project.closed ? : }  {project.closed ? 'Cerrado' : 'Abierto'}  -   {project.published ? : }  {project.published ? 'Publico' : 'Oculto'}  -   {project.commentsCount} Aport{project.commentsCount > 1 ? 'es' : 'e'}  -   - {project.currentVersion.version} {project.currentVersion.version > 1 ? 'Versiones' : 'Versión'} + {project.currentVersion.version} {project.currentVersion.version > 1 ? 'Versiones' : 'Versión'}  -   + {project.apoyosCount} Apoyo{project.apoyosCount != 1 && 's'}
Fecha de creación: {formatDate(project.createdAt)}  -   Fecha de cierre: {formatDate(project.currentVersion.content.closingDate)} @@ -110,6 +111,11 @@ export default ({ project }) => (

{project.currentVersion.version} {project.currentVersion.version > 1 ? 'Versiones' : 'Versión'}

+ +

+ {project.apoyosCount} Apoyo{project.apoyosCount != 1 && 's'} +

+
{formatDate(project.createdAt)} diff --git a/components/validar-apoyo/component.js b/components/validar-apoyo/component.js new file mode 100644 index 0000000..6af82ad --- /dev/null +++ b/components/validar-apoyo/component.js @@ -0,0 +1,98 @@ +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' +import Link from 'next/link' +import getConfig from 'next/config' +const { publicRuntimeConfig: { API_URL } } = getConfig() +import fetch from 'isomorphic-unfetch' +import Masonry from 'react-masonry-component'; +import Card from '../../components/card/component' + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 60px 0 60px; +` +const Box = styled.div` + font-size: 1.8rem; + padding: 20px 40px; + background-color: #74acce; + color: white; + margin-bottom: 20px; +` +const Note = styled.div` + font-size: 1.6rem; + a { + color:#5c97bc + font-weight: bold; + } + a:hover,a:active,a:focus { + color:#2f6a8e + } +` + +const ProjectsTitle = styled.h1` + margin-top: 50px; +` + +export default () => { + const [isValidado, setIsValidado] = useState(null); + const [project, setProject] = useState(null); + const [projects, setProjects] = useState(null); + + // mount event + useEffect(() => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const v = urlParams.get('v'); + + fetch(`${API_URL}/api/v1/documents/apoyo-anon-validar/${v}`) + .then(r => r.json()) + .then(j => { + if (j.error){ + setIsValidado(false) + fetch(`${API_URL}/api/v1/documents`).then(r => r.json()).then(j => setProjects(j.results)) + }else { + let rProject = j.document + setProject(rProject) + setIsValidado(true) + fetch(`${API_URL}/api/v1/documents`).then(r => r.json()).then(j => setProjects(j.results.filter(d => d._id != rProject._id))) + } + }) + }, []) + + return ( + + + + { isValidado === null ? + 'Validando apoyo...' + : ( + isValidado ? + '¡Su apoyo ha sido validado con éxito!' + : + 'No se ha podido validar su apoyo' + ) + } + + + + Haga click  + + aquí + +  para volver al {project && 'proyecto' || 'inicio'} + + + Otros proyectos que puedes apoyar: + {projects && + + {projects.map((p, i) => ( + + ))} + + } + + + ) +} diff --git a/containers/general-container/component.js b/containers/general-container/component.js index 8b9ffa9..63d13f4 100644 --- a/containers/general-container/component.js +++ b/containers/general-container/component.js @@ -54,6 +54,46 @@ class GeneralContainer extends Component { } } + apoyarProyecto = async (anonData) => { + // anonData == {token, nombre_apellido, email, captcha} + const { authenticated, keycloak } = this.props.authContext + const { project } = this.state + + if (!project) + return + + let projectId = project.document._id + let url + let headers = { 'Content-Type': 'application/json' } + let body + + if (authenticated && keycloak && keycloak.token){ + url = `${API_URL}/api/v1/documents/${projectId}/apoyar` + Object.assign(headers, { + Authorization: `Bearer ${keycloak.token}` + }) + body = {} + } else { + url = `${API_URL}/api/v1/documents/${projectId}/apoyar-anon` + body = { + token: anonData.token, + nombre_apellido: anonData.nombre_apellido, + email: anonData.email, + captcha: anonData.captcha, + } + } + + return fetch(url, { + headers, + method: 'POST', + body: JSON.stringify(body) + }).then(async (res) => { + if (res.status == 200) + this.fetchDocument(projectId, keycloak && keycloak.token) + return res + }) + } + render () { return ( @@ -61,7 +101,12 @@ class GeneralContainer extends Component {
- +