diff --git a/CHANGELOG.md b/CHANGELOG.md index 650fec37..1ac6ca9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,212 @@ - # Changelog +### 3.0.0 + +> **IMPORTANTE: HAY NUEVAS VARIABLES DE ENTORNO, POR FAVOR VERIFICAR EL ARCHIVO `.env.dist` DEL NOTIFIER** + +En las variables de entorno del notifier, se agregaron las variables: + +```bash +BOTTLENECK_ENABLE= #true|false +BOTTLENECK_MIN_TIME= #in milliseconds +BOTTLENECK_MAX_CONCURRENT= #number of concurrent jobs +``` + +Dado que las notificaciones en esta nueva version tenderá a enviar grandes cantidades de emails, agregamos un "throttle" para evitar que el servidor de correo nos bloquee por abuso. Es importante que se configure correctamente, ya que si no se configura, el throttle no se activa y el servidor de correo puede colapsar. La forma de configurarlo es pensar "cuantas notificaciones deseamos que se envien cada X tiempo". Por ejemplo, si nuestro servidor de SMTP nos limita a que no se envien mas de 10 emails cada 5 segundos, entonces podriamos configurarlo para que, siendo precabidos, en una venta de 2.5 segundos se envien alrededor de 4 mails. Para esto, configurariamos las variables de entorno de la siguiente forma: + +```bash +BOTTLENECK_ENABLE= true +BOTTLENECK_MIN_TIME= 2500 +BOTTLENECK_MAX_CONCURRENT= 4 +``` + +Entonces nos aseguramos que a los 5 segundos no hayamos enviado mas de 10 mails, y que en 2.5 segundos no hayamos enviado mas de 4 mails. Esto es solo un ejemplo, y es importante que se configure correctamente para evitar que el servidor de correo nos bloquee por abuso de floodings. + +Si no se desea activar throttle, se puede desactivar con `BOTTLENECK_ENABLE=false` y el resto de las variables no se toman en cuenta. + +**Listado de cambios:** + +* NEW: Nuevo procedimiento de init() del sistema: Implementacion de "migrations", para realizar migraciones mas concretas y de forma secuenciales. Esto permite que el sistema pueda ser actualizado de forma mas sencilla y tener mejor auditoria de los cambios que se realizan en la base de datos. +* MIGRATION 001 & MIGRATION 002: Son migraciones que se encargan de setear datos basicos al iniciar el sistema por primera vez. Si el sistema ya esta iniciado, estas migraciones no se saltean y se marcan como ejecutadas. La 001 responde a crear la comunidad, si ya existe, no se crea. La 002 se encarga de setear las tags, si ya existen, no se crean. +* MIGRATION 003: Esta migracion fuerza todas las tags/etiquetas de intereses de todos los usuarios de la DB. +* MIGRATION 004: Esta migracion fuerza las notificaciones de proyectos populares en todos los usuarios DB. +* MIGRATION 005: Esta migracion se encarga de setear en los documentos existentes de la base de datos si ya son populares o si no. Si ya lo son, se setea que el mail de popular "ya" fue enviado (esto es para evitar que proyectos del pasado sean "populares" cuando ya están cerrados.) +* MIGRATION 006: Esta migracion agrega el campo "authors" en los fields de los usuarios (necesario para que se puedan suscribir a sus diputados favoritos) +* MIGRATION 007: Esta migracion fuerza que TODOS los usuarios (si no lo tenian) tengan activadas las notificaciones por tags/etiquetas de intereses. +* NEW: Reordenados los "customForms" de los fields de usuarios y documentos en archivos separados, para mejor control de los mismos. +* NEW: Agregado en el customForm de users el campo "popularNotification" para guardar la preferencia si el usuario desea ser notificado por proyectos populares +* NEW: Agregado en el customForm de users el campo "authors" para guardar la lista de usuarios +* NEW: Agregado en el customForm de documents el campo "popular" para guardar si el documento es popular o no +* NEW: Nuevo segmento "Mis notificaciones" en el perfil de un usuario: Ahora los usuarios tienen separado de la seccion "Editar perfil" la configuracion de sus notificaciones, que son 4: Activar notificaciones por etiquetas de interes / Seleccion de etiquetas de interes / Activar notificaciones por proyectos populares / Diputados suscriptos. +* NEW: Nueva notificacion: "Proyectos populares": Ahora cuando un proyecto se vuelve popular (esto significa, que consigue 30 apoyos, o 10 comentarios en su fundamento, o 5 aportes en el articulado) se envia un mail a todos los usuarios que tengan activada la opcion de recibir notificaciones por proyectos populares. +* NEW: Agregado nuevo boton en vista de proyecto: "Suscribirse al/Desuscribirse del diputado" para que los usuarios puedan suscribirse a los diputados que quieran, y recibir notificaciones cuando estos publiquen un nuevo proyecto. +* NEW: Agregado nuevo boton al visitar el perfil de un diputado "Suscribirse al/Desuscribirse del diputado" (Similar al punto anterior) +* NEW: Ahora al agregar una nueva etiqueta, automaticamente todos los usuarios van a ser suscriptos a esta etiqueta. +* NEW: Ahora se guarda el lastLogin de los usuarios, para saber cuando fue la ultima vez que se loguearon. +* NEW: Nuevos usuarios que se registran automaticamente estaran suscripto a todas las etiquetas de interes, y a recibir notificaciones por proyectos populares. (NO asi, no se los suscribe a todos los diputados, eso es decision especifica del usuario) +* NEW - Notifier: Nueva notificacion del tipo "document-popular" para documentos que se vuelven populares. Se le notifica a los usuarios que tienen la opcion de recibir notificaciones por proyectos populares activada. +* NEW - Notifier: Ahora la notificacion de "document-published" o documento publicado contempla a todos los usuarios que: Estan suscriptos a notificaciones por etiquetas y el documento tiene una etiqueta que le interesa al usuario, o bien, el usuario esta suscripto a notificaciones por diputados y el documento fue publicado por un diputado al que el usuario esta suscripto. +* NEW - Notifier: Se agrego al mail de notificacion de cierre de documento, ademas de que ya se veia la cantidad de comentarios, la cantidad de aportes al articulado y la cantidad de apoyos al proyecto. +* NEW - Notifier: Se agrego al mail de notificacion de documento publicado el recuadro del proyecto (que generalmente se mostraba en otras plantillas pero estaba faltando en la de publicacion del proyecto) +* NEW - Notifier: Agregado **bottleneck** como dependencia, que es un paquete que se encarga de limitar la cantidad de mails que se envian por segundo, para evitar que el servidor de correo nos bloquee por abuso. **IMPORTANTE: HAY NUEVAS VARIABLES DE ENTORNO, POR FAVOR VERIFICAR EL ARCHIVO `.env.dist` DEL NOTIFIER**. Este throttle es opcional pero vital para evitar que el servidor de correo nos bloquee por abuso. Es importante que se configure correctamente, ya que si no se configura, el throttle no se activa y el servidor de correo puede colapsar. +* NEW - Notifier: Se cambio el uso de la palabra "propuesta" por "proyecto" en los mails de notificaciones. +* NEW - Notifier: Se cambiaron los titulos de los emails de notificaciones para que sean mas descriptivos. Ver Nota al final de este changelog. +* FIX: Se arreglo un problema de que al eliminar una etiqueta, la misma no se borraba de la lista de tags de los documentos. +* FIX: Se arreglo un problema de que al eliminar una etiqueta, la misma no se borraba de la lista de tags/etiquetas de los usuarios que se suscribieron a las mismas. +* FIX: Se arreglo un problema al apoyar un proyecto de forma anonima: El usuario recibia un correo para validar su apoyo, al ser redirigido a la pagina web, ocurrian doble HTTP GET al link, donde uno validaba, pero el ultimo devolvia error porque ya el primero lo habia validado, y el usuario siempre veia "No se encontro su apoyo" cuando en realidad el mismo fue procesado por el primer GET. Se soluciono cambiando el endpoint de HTTP GET a HTTP POST y aplicando un setTimeout en la vista de 3 segundos antes de enviar el POST. +* FIX: Para notificaciones cuando un proyeto se publica, en el momento que se enviaba el pedido del CORE al NOTIFIER, el modulo de NOTIFIER corrobora si el mail habia sido enviado, como el CORE estaba seteando el flag `publishedMailSent`, el NOTIFIER no enviaba el mail porque este flag evitaba su envio. Ahora se cambio para que los flags (tanto `publishedMailSent` y `popularMailSent`) los setee el NOTIFIER, y no el CORE. +* FIX - Web: Ahora en el admin, al crear una nueva etiqueta, el slug se crea en el backend de mejor forma que como se creaba en el backend. +* FIX - Web: Ahora en el admin, en la vista de etiquetas, las mismas estan ordenadas alfabeticamente. +* FIX - Web: Ahora en la pagina principal, las etiquetas estan ordenadas alfabeticamente. +* FIX - Web: Las etiquetas del recuadro de un proyeto se achicaron los espacios y compactaron las etiquetas, ya que se rompia y escapaban del recuadro en algunos casos. Tambien se quito el letter-spacing que agregaba confusion a etiquetas grandes. +* FIX - Web: Corregido el titulo "tags" por "Etiquetas" en la vista de admin de Etiquetas +* FIX - Web: Corregido el titulo "users" por "Usuarios" en la vista de admin de Usuarios +* FIX - Web: En la seccion de "Usuarios" del admin, no se podia diferenciar que era cada usuario, si un usuario normal o si era un admin o si un diputado. Ahora visualmente se puede diferenciar +* FIX - Web: En la seccion de "Usuarios" del admin el buscador permite buscar a cualquier usuario, pero el placeholder especificamente decia que podia buscar diputados por nombre. +* FIX - Notifier: Las variables de entorno se estaban cargando de forma inconsistente e incorrecta. +* FIX - Notifier: Se agregaron mejoras en el logging del notifier, mas informacion para poder debuggear en caso de error. +* FIX - Notifier: Las notificaciones del cierre de documentos no estaba tomando en cuenta las personas que apoyaron el proyecto. A partir de ahora los usuarios que reciben la notificacion que un proyeto cierra es la union de: Los usuarios que participaron comentando en el fundamento + Los usuarios que aportaron en el articulado + Los usuarios que dieron like a un comentario + Los usuarios que apoyaron el proyecto. +* NOTA: La notificacion de proyectos populares se envia a todos los usuarios que tengan activada la opcion de recibir notificaciones por proyectos populares, completamente ajeno si siguen o no al diputado, o si estan o no suscriptos a notificaciones por etiquetas de interes. +* NOTA: La notificacion de proyectos populares se envia UNA sola vez. Comentarios, aportes o apoyos posteriores no vuelven a enviar la notificacion. + +##### Titulos de las notificaciones + +* Título para notificación al autor del proyecto sobre nuevo comentario: 'Ha recibido un nuevo comentario en su proyecto de Leyes Abiertas' +* Título para notificación al usuario autor de un aporte en el articulado de un proyecto el cual el diputado ha marcado como resuelto: 'Su comentario en un proyecto de Leyes Abiertas ha sido marcado como resuelto' +* Título para notificación al usuario autor de un aporte en el articulado de un proyecto el cual el diputado ha dejado un like: 'Su comentario en un proyecto de Leyes Abiertas ha sido marcado como relevante' +* Título para notificación al usuario autor de un aporte en el articulado o de un comentario en los fundamentos de un proyecto el cual el diputado ha respondido: 'Su comentario en un proyecto de Leyes Abiertas recibió una respuesta' +* Título para notificación al usuario autor de un comentario en el articulado de un proyecto el cual el diputado ha marcado como aporte: 'Su comentario en un proyecto de Leyes Abiertas ha sido marcado como aporte' +* Título para notificación a todos los usuarios que estan suscriptos a etiquetas del proyecto o al autor del proyecto y que tienen sus notificaciones habilitadas: 'Nuevo proyecto publicado en Leyes Abiertas' +* Título para notificación a todos los usuarios que estan suscriptos a ser notificados cuando un proyecto se vuelve popular: 'Un proyecto en Leyes Abiertas está volviendose popular' +* Título para notificación a un usuario no registrado para validar su apoyo: '¡Último paso para apoyar el proyecto en Leyes Abiertas!' + + +Compatible con: +* `leyesabiertas-web:3.0.0` +* `leyesabiertas-core:3.0.0` +* `leyesabiertas-notifier:3.0.0` +* `leyesabiertas-keycloak:2.0.0` + +### 2.1.0 + +* Agregado sección de metricas para administradores +* Agregado en los datos del usuario `lastLogin` +* Cambio en middleware `bindUserToSession` para guardar la fecha de lastLogin + +#### Metricas de autores: + +* Cantidad de proyectos por autor +* Cantidad de proyectos creados en X año por autor + +#### Metricas de etiquetas: + +* Cantidad de proyectos por etiquetas ordenadas de forma descendiente +* Cantidad de proyectos creados en X año por etiqueta +* (Al seleccionar una etiqueta) Lista de proyectos +* (Al seleccionar una etiqueta) Lista  creados en X año +* Lista de proyectos sin etiquetas + +#### Metricas de usuarios + +* Cantidad de usuarios registrados +* Cantidad de usuarios comunes (sin rol admin o autor) +* Cantidad de usuarios registrados que se registraron en X año +* Cantidad de usuarios comunes que se registraron en X año +* Lista de usuarios admin +* Lista de usuarios autores + +#### Metricas de interaccion por proyecto + +* Lista de proyectos ordenados por mayor a menor interaccion en total: + +``` +* Total de comentarios en la fundamentacion +* Total de comentarios en aportes +* Total de likes (en comentarios de la fundamentacion y en aportes) +* Total de apoyos +* Total de interacciones (comentarios+apoyos+likes) +``` + +* Posibilidad de filtrar por + +``` +* Año de creacion de proyecto +* Etiqueta +* Autor +``` + +#### * Descarga de datasets: + +* Listado completo de usuarios con: +``` +* id +* nombre +* apellido +* email +* ocupacion +* genero +* fecha de nacimiento +* provincia +* partido +* notificaciones_activadas +* fecha de creacion +* fecha de actualizacion +* fecha de ultimo login +``` +* Listado de proyectos con interacciones y autor: +``` +* id +* titulo +* version +* tags +* autorNombre +* autorEmail +* apoyos +* likes +* comentariosFundamentos +* comentariosAportesArticulado +* totalInteracciones +* fechaCreacion +* fechaCierre +``` + +Compatible con: +* `leyesabiertas-web:2.1.0` +* `leyesabiertas-core:2.1.0` +* `leyesabiertas-notifier:2.0.0` +* `leyesabiertas-keycloak:2.0.0` + +### 2.0.1 + +- Ordena documentos por orden de cierre + +Ultimos cambios de frontend: + +- Banners en acerca de +- Cambio logo +- Sistematización de botones en home +- ajustes en pagina apoyo sin registro +- cambio logo navbar + + +Compatible con: + +* `leyesabiertas-web:2.0.1` +* `leyesabiertas-core:2.0.1` +* `leyesabiertas-notifier:1.9.1` +* `leyesabiertas-keycloak:1.8.0` + +### 2.0.0 + +> TO BE DONE + +Compatible con: + +* `leyesabiertas-web:2.0.0` +* `leyesabiertas-core:2.0.0` +* `leyesabiertas-notifier:1.9.1` +* `leyesabiertas-keycloak:1.8.0` + ### 1.9.4 2021-06-10: Este pedido fue porque habian usado tag ambiental para representar el Foro Legislativo Ambiental diff --git a/components/admin-drawer/component.js b/components/admin-drawer/component.js index f6f6c097..8632daf0 100644 --- a/components/admin-drawer/component.js +++ b/components/admin-drawer/component.js @@ -54,7 +54,7 @@ const buttons = [ }, { "key":4, - 'name':'Metricas', + 'name':'Métricas', 'value':'metric' } diff --git a/components/admin-metric/component.js b/components/admin-metric/component.js index ee171e37..1eb7d352 100644 --- a/components/admin-metric/component.js +++ b/components/admin-metric/component.js @@ -119,24 +119,24 @@ class MetricAdmin extends Component { const { showMetricByAuthor, showMetricByTags, showMetricUsers, showMetricInteractions } = this.state return ( - Metricas + Métricas this.setState({ showMetricByAuthor: !this.state.showMetricByAuthor })}> - Metricas por autor + Métricas por autor { showMetricByAuthor && } this.setState({ showMetricByTags: !this.state.showMetricByTags })}> - Metricas por etiquetas + Métricas por etiquetas { showMetricByTags && } this.setState({ showMetricInteractions: !this.state.showMetricInteractions })}> - Metricas de interacciónes en proyectos + Métricas de interacciones en proyectos { @@ -147,7 +147,7 @@ class MetricAdmin extends Component { ) } this.setState({ showMetricUsers: !this.state.showMetricUsers })}> - Metricas de usuarios + Métricas de usuarios { diff --git a/components/admin-metric/metricsTags.js b/components/admin-metric/metricsTags.js index 8b1720d2..bbaf4b2c 100644 --- a/components/admin-metric/metricsTags.js +++ b/components/admin-metric/metricsTags.js @@ -328,6 +328,7 @@ class MetricsTags extends Component { } listDocuments (tagName, documents) { + // scroll to id=metricTitle this.setState((prevState) => { return { showDocumentsTagName: tagName, @@ -338,6 +339,9 @@ class MetricsTags extends Component { limit: prevState.limit, totalPages: Math.ceil(documents.length / prevState.limit) } + },() => { + const element = document.getElementById('metricTitle') + element.scrollIntoView({ behavior: 'smooth', block: 'center' }) }) } @@ -457,7 +461,7 @@ class MetricsTags extends Component { { showDocumentsTagName && (
- + Proyectos con etiqueta "{showDocumentsTagName}" diff --git a/components/admin-projects/component.js b/components/admin-projects/component.js index 73cbea52..6c79a6ea 100644 --- a/components/admin-projects/component.js +++ b/components/admin-projects/component.js @@ -12,181 +12,6 @@ import { plus } from 'react-icons-kit/feather' const { publicRuntimeConfig: { API_URL } } = getConfig() -const getClosingDate = () => { - let closingDate = new Date() - closingDate.setDate(closingDate.getDate() + 30) - closingDate.setHours(0, 0, 0, 0) - return closingDate.toISOString() -} - -const newDocument = { - 'published': false, - 'closed': false, - 'customForm': 'project-form', - 'content': { - 'title': 'Mi nuevo proyecto', - 'imageCover': null, - 'youtubeId': null, - 'customVideoId': null, - 'sendTagsNotification': true, - 'fundation': { - 'object': 'value', - 'document': { - 'object': 'document', - 'data': { - }, - 'nodes': [ - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Esta sección es un espacio para redactar un texto que sirve para presentar el proyecto, explicar el contexto (de donde surge, su importancia, etc.), e invitar la ciudadanía a participar. Es muy importante mencionar qué tipo de aportes ciudadanos se esperan. El proyecto tiene que estar explicado de manera muy simple, la redacción debe ser fácil de entender.', - 'marks': [ - ] - } - ] - } - ] - } - ] - } - }, - 'articles': { - 'object': 'value', - 'document': { - 'object': 'document', - 'data': { - }, - 'nodes': [ - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 1º.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sed purus justo. Nam tempus ligula nec est scelerisque aliquet. Phasellus pretium rhoncus pharetra. Duis dapibus felis neque.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 2°.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Fusce elementum posuere dolor id mattis. Sed magna arcu, rutrum eu pellentesque nec, feugiat sit amet lorem. Fusce volutpat, dolor a pretium fermentum, felis justo rhoncus nisl, vel mollis est diam mollis nisl. Sed aliquet erat sed ipsum lacinia, feugiat interdum ante pulvinar. Integer ut consectetur velit.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 3°.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'In id neque posuere, dictum arcu vitae, euismod nulla. Integer eu euismod ipsum. In aliquet nisl mi, nec vulputate urna hendrerit eu. Integer in mi at quam luctus posuere. Integer magna sem, viverra non ultrices vitae, varius in mi.', - 'marks': [ - ] - } - ] - } - ] - } - ] - } - }, - 'closure': null, - 'closingDate': getClosingDate() - } -} - const StyledProjectsAdmin = styled.div` ` @@ -449,7 +274,8 @@ toggleSort = (parameter, value) => { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.props.authContext.keycloak.token }, - 'body': JSON.stringify(newDocument) + // no body, now the API has the project base + // 'body': JSON.stringify(newDocument) }) .then((res) => { if (!res.ok) { diff --git a/components/admin-tags/component.js b/components/admin-tags/component.js index fbcb7c55..9bafe23c 100644 --- a/components/admin-tags/component.js +++ b/components/admin-tags/component.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' import styled from 'styled-components' +import getConfig from 'next/config' import WithDocumentTagsContext from '../../components/document-tags-context/component' -import getConfig from 'next/config' import TagsList from '../../elements/tag-list/component' import TagNew from '../../elements/tag-form/component' import Modal from '../modal/component' @@ -11,9 +11,6 @@ import TitleContent from '../title-content-admin/component' const { publicRuntimeConfig: { API_URL } } = getConfig() - - - const StyledTagsAdmin = styled.div` ` @@ -32,11 +29,10 @@ color: #4C4C4E; border-radius:5px; font-weight: 600; font-family: var(--italic); -padding:8px; +padding: 5px 10px; font-size:12px line-height: 15px; text-align: center; -letter-spacing: 1.1px; text-transform: capitalize; ` @@ -45,7 +41,7 @@ margin:23px 8px; min-width: 125px; max-width: 230px; height: 39px; -background-color: ${(props) => props.type === 'deleteButton' ? '#CF1419': '#5c97bc'}; +background-color: ${(props) => props.type === 'deleteButton' ? '#CF1419' : '#5c97bc'}; font-size: 1.4rem; color: var(--white); border-style: none; @@ -54,92 +50,85 @@ padding: 0 2rem; font-family: var(--bold); ` -class TagsAdmin extends Component{ +class TagsAdmin extends Component { state = { - allTags:null, - modalActive:false, - tagToDelete:null} - - componentDidMount(){ + allTags: null, + modalActive: false, + tagToDelete: null } + componentDidMount () { this.fetchtags() } - fetchtags = () =>{ + fetchtags = () => { this.props.fetchDocumentTags() - .then(documentTags => { - const parsedTags = documentTags.map(documentTag => ({ id: documentTag._id, text: documentTag.name })) + .then((documentTags) => { + const parsedTags = documentTags.map((documentTag) => ({ id: documentTag._id, text: documentTag.name })) - this.setState({ - allTags: parsedTags + this.setState({ + allTags: parsedTags + }) }) - }) - .catch(err=>console.error(err)) + .catch((err) => console.error(err)) } - deleteTag = (tag)=>{ + deleteTag = (tag) => { this.setState({ tagToDelete: tag, modalActive: true }) } - confirmDeleteTag = async () => { - await fetch(`${API_URL}/api/v1/document-tags/${this.state.tagToDelete.id}`,{ - method:'DELETE', + await fetch(`${API_URL}/api/v1/document-tags/${this.state.tagToDelete.id}`, { + method: 'DELETE', headers: { 'Authorization': 'Bearer ' + this.props.token - }, + } }) - this.setState({tagToDelete:null,modalActive: false}) + this.setState({ tagToDelete: null, modalActive: false }) this.fetchtags() } - addTag = async (newTag) =>{ - try{ - if(newTag.length > 0){ - - - await fetch(`${API_URL}/api/v1/document-tags`,{ - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + this.props.token - }, - body:JSON.stringify( - {'name':newTag, - 'key':newTag.replace(/ /g,"-")} - ) - }) - this.fetchtags() - }else{ - throw Error() - } - }catch(error){ - console.log(error); + addTag = async (newTag) => { + try { + if (newTag.length > 0) { + await fetch(`${API_URL}/api/v1/document-tags`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + this.props.token + }, + body: JSON.stringify({ name: newTag }) + }) + this.fetchtags() + } else { + throw Error() + } + } catch (error) { + console.log(error) } } - render(){ - const {allTags, modalActive, tagToDelete} = this.state - return( + render () { + const { allTags, modalActive, tagToDelete } = this.state + return ( - tags + Etiquetas {allTags && } - {modalActive && this.setState({modalActive:false})} - title={`¿seguro desea eliminar ${tagToDelete.text}?`} - footer={
- this.setState({modalActive:false})} type='cancel'>Cancelar - this.confirmDeleteTag()} type='deleteButton'>Eliminar -
}/>} + {modalActive && this.setState({ modalActive: false })} + title={`¿seguro desea eliminar ${tagToDelete.text}?`} + footer={
+ this.setState({ modalActive: false })} type='cancel'>Cancelar + this.confirmDeleteTag()} type='deleteButton'>Eliminar +
} />}
) } - } +} TagsAdmin.propTypes = { } diff --git a/components/admin-users/component.js b/components/admin-users/component.js index 3fc845ea..f1a9c3e9 100644 --- a/components/admin-users/component.js +++ b/components/admin-users/component.js @@ -139,8 +139,8 @@ class UsersAdmin extends Component{ const { usersList, fetching, fetchMoreAvailable } = this.state return( - users - this.toggleSort('search', e.target.value)} /> + Usuarios y Diputados + this.toggleSort('search', e.target.value)} /> {usersList && usersList.map((user, idx) => diff --git a/components/card-users/component.js b/components/card-users/component.js index de6b0228..7f30127c 100644 --- a/components/card-users/component.js +++ b/components/card-users/component.js @@ -2,11 +2,12 @@ import React from 'react' import Link from 'next/link' import PropTypes from 'prop-types' import styled from 'styled-components' +import router from 'next/router' import CardUserHeader from '../../elements/card-user-header/component' import CardUserActions from '../../elements/card-user-actions/component' -import router from 'next/router' const CardContainer = styled.div` +position: relative; margin: 0 1% 30px; width: 23%; box-shadow: 0 4px 20px 0 rgba(0,0,0,0.05); @@ -27,27 +28,63 @@ position: relative; @media (max-width: 600px) { width: 100%; } +${(props) => props.hasRole && ` + border: solid 2px #5c97bc; + // box-shadow: 0 4px 20px 0 rgba(92,151,188,0.05); + `} +` + +const UserTagsWrapper = styled.div` + position: absolute; + bottom: 0; + left: 0; + display: flex; + flex-direction: column; + align-items:flex-start; +` + +const UserTag = styled.div` + font-size: 10px; + background-color: #5c97bc; + color: #FFF; + padding: 2px 6px 1px 4px; + font-family: var(--bold); + // text-transform: uppercase; + border-top-right-radius: 4px; ` const CardUser = ({ user }) => { - const edit = ()=>{ - router.push(`/admin?section=userEdit&user=${user._id}`); - } - const projects = ()=>{ + const edit = () => { + router.push(`/admin?section=userEdit&user=${user._id}`) + } + const projects = () => { router.push(`/admin?section=projects&user=${user._id}`) - } + } + + if (!user || !user._id) return null + + const userHasRole = user && user.roles && Array.isArray(user.roles) && user.roles.length > 0 && (user.roles.includes('admin') || user.roles.includes('accountable')) return ( - - - - - + + { + userHasRole && + { + user && user.roles && user.roles.includes('admin') && Admin + } + { + user && user.roles && user.roles.includes('accountable') && Diputado + } + + } + + + ) } CardUser.propTypes = { - user: PropTypes.object.isRequired, - + user: PropTypes.object.isRequired + } export default CardUser diff --git a/components/profile/component.js b/components/profile/component.js index 25e376d1..93ed8832 100644 --- a/components/profile/component.js +++ b/components/profile/component.js @@ -12,21 +12,22 @@ import ProfileLabel from '../../elements/profile-label/component' import ProfileInput from '../../elements/profile-input/component' import ProfileSelect from '../../elements/profile-select/component' import ProfileButtonWrapper from '../../elements/profile-button-wrapper/component' -import ProfileTags from '../../elements/profile-tags/component' +// import ProfileTags from '../../elements/profile-tags/component' import SubmitInput from '../../elements/submit-input/component' -import WithDocumentTagsContext from '../../components/document-tags-context/component' +// import WithDocumentTagsContext from '../../components/document-tags-context/component' +import SubscribeButtonProfile from '../../components/subscribe-button-profile/component' -const TagsNotificationCheckboxDiv = styled.div` - width: 350px; - display: flex; - line-height: 15px; - margin-top: 3px; - font-size:13px; - & > input { - margin-right: 7px; - margin-bottom: auto; - } -` +// const TagsNotificationCheckboxDiv = styled.div` +// width: 350px; +// display: flex; +// line-height: 15px; +// margin-top: 3px; +// font-size:13px; +// & > input { +// margin-right: 7px; +// margin-bottom: auto; +// } +// ` const ButtonLink = styled.button` background-color: #5c97bc; @@ -69,14 +70,14 @@ class Profile extends Component { province: '', editMode: false, files: [], - allTags: [], - tags: [], - tagsMaxReached: false, - tagsNotification: '' + // allTags: [], + // tags: [], + // tagsMaxReached: false, + // tagsNotification: '' } async componentWillMount () { - this.setState({ allTags: await this.props.fetchDocumentTags() }) + // this.setState({ allTags: await this.props.fetchDocumentTags() }) } componentDidMount () { @@ -87,8 +88,8 @@ class Profile extends Component { party: user.fields && user.fields.party ? user.fields.party : '', birthday: user.fields && user.fields.birthday ? user.fields.birthday : '', province: user.fields && user.fields.province ? user.fields.province : '', - tags: user.fields && user.fields.tags ? user.fields.tags : [], - tagsNotification: user.fields && user.fields.tagsNotification ? user.fields.tagsNotification : '' + // tags: user.fields && user.fields.tags ? user.fields.tags : [], + // tagsNotification: user.fields && user.fields.tagsNotification ? user.fields.tagsNotification : '' }) } @@ -115,9 +116,9 @@ class Profile extends Component { party: user.fields && user.fields.party ? user.fields.party : '', birthday: user.fields && user.fields.birthday ? user.fields.birthday : '', province: user.fields && user.fields.province ? user.fields.province : '', - tags: user.fields && user.fields.tags ? user.fields.tags : [], - tagsMaxReached: false, - tagsNotification: user.fields && user.fields.tagsNotification ? user.fields.tagsNotification : '' + // tags: user.fields && user.fields.tags ? user.fields.tags : [], + // tagsMaxReached: false, + // tagsNotification: user.fields && user.fields.tagsNotification ? user.fields.tagsNotification : '' }) } @@ -130,20 +131,20 @@ class Profile extends Component { }) } - handleTagClick = (tag) => { - if (this.state.tagsMaxReached) - this.setState({tagsMaxReached: false}) + // handleTagClick = (tag) => { + // if (this.state.tagsMaxReached) + // this.setState({tagsMaxReached: false}) - const clickedTagId = tag._id - if (this.state.tags.includes(clickedTagId)) - this.setState((prevState) => ({tags: prevState.tags.filter(tagId => tagId != clickedTagId)})) - else { - if (this.state.tags.length == 6) - this.setState({tagsMaxReached: true}) - else - this.setState((prevState) => ({tags: prevState.tags.concat(clickedTagId)})) - } - } + // const clickedTagId = tag._id + // if (this.state.tags.includes(clickedTagId)) + // this.setState((prevState) => ({tags: prevState.tags.filter(tagId => tagId != clickedTagId)})) + // else { + // if (this.state.tags.length == 6) + // this.setState({tagsMaxReached: true}) + // else + // this.setState((prevState) => ({tags: prevState.tags.concat(clickedTagId)})) + // } + // } handleSubmit = async (e) => { e.preventDefault() @@ -153,9 +154,9 @@ class Profile extends Component { gender: this.state.gender || '', birthday: this.state.birthday || '', province: this.state.province || '', - party: this.state.party || '', - tags: this.state.tags || '', - tagsNotification: this.state.tagsNotification || '' + party: this.state.party || '' + // tags: this.state.tags || '', + // tagsNotification: this.state.tagsNotification || '' } } if (this.state.avatar) { @@ -168,13 +169,13 @@ class Profile extends Component { jump(-1000) } - toggleTagsCheckboxChange = () => { - this.setState(({ tagsNotification }) => ( - { - tagsNotification: !tagsNotification, - } - )); - } + // toggleTagsCheckboxChange = () => { + // this.setState(({ tagsNotification }) => ( + // { + // tagsNotification: !tagsNotification, + // } + // )); + // } render () { @@ -184,8 +185,10 @@ class Profile extends Component { {`${user.surnames}, ${user.names}`} - { isOwner && !this.state.editMode ? Editar perfil : null } + {/* Log stuff */} { isLoading ?

...

: null} + { !isOwner && user.roles.includes('accountable') && } + { isOwner && !this.state.editMode && this.toggleEdit()}>Editar perfil } { this.state.editMode ?
@@ -246,7 +249,7 @@ class Profile extends Component { : null } - + {/* Etiquetas de interés - + */} + party={project.author.fieds && project.author.fields.party ? project.author.fields.party : ''} /> + /> {isAuthor && @@ -142,7 +163,6 @@ const ProjectHeader = ({ project, section, isPublished, isAuthor, setPublish, to {project.currentVersion.content.title} - {/* {currentSection === '/propuesta' && -
+ Presentación Artículos -
+ {/* @@ -173,6 +193,7 @@ const ProjectHeader = ({ project, section, isPublished, isAuthor, setPublish, to */} + @@ -180,10 +201,10 @@ const ProjectHeader = ({ project, section, isPublished, isAuthor, setPublish, to } {currentSection === '/versiones' && -
+ Presentación Artículos -
+ {/* @@ -192,9 +213,9 @@ const ProjectHeader = ({ project, section, isPublished, isAuthor, setPublish, to */} + -
} diff --git a/components/project-toggle-publish/component.js b/components/project-toggle-publish/component.js index b81892bd..a3ca70a7 100644 --- a/components/project-toggle-publish/component.js +++ b/components/project-toggle-publish/component.js @@ -5,9 +5,9 @@ import Icon from 'react-icons-kit' import { clockO } from 'react-icons-kit/fa/clockO' import { toggle } from 'react-icons-kit/ionicons/toggle' import { toggleFilled } from 'react-icons-kit/ionicons/toggleFilled' +import getConfig from 'next/config' import WithUserContext from '../with-user-context/component' import Alert from '../../elements/alert/component' -import getConfig from 'next/config' const { publicRuntimeConfig: { API_URL } } = getConfig() @@ -107,7 +107,7 @@ const LoadingClick = styled.span` ` class TogglePublished extends Component { - constructor(props) { + constructor (props) { super(props) this.state = { showAlert: false, @@ -159,7 +159,7 @@ class TogglePublished extends Component { }) } - render() { + render () { const { isPublished } = this.props const { showAlert, alertType, alertText, isLoading } = this.state return ( @@ -176,7 +176,7 @@ class TogglePublished extends Component { } { - isLoading && + isLoading &&   Guardado... diff --git a/components/subscribe-button copy/component.js b/components/subscribe-button copy/component.js new file mode 100644 index 00000000..fe09d571 --- /dev/null +++ b/components/subscribe-button copy/component.js @@ -0,0 +1,133 @@ +import React, { Component, Fragment } from 'react' +// import PropTypes from 'prop-types' +import styled from 'styled-components' +import fetch from 'isomorphic-unfetch' +import getConfig from 'next/config' +import WithUserContext from '../with-user-context/component' +import Icon from 'react-icons-kit' +import { userPlus } from 'react-icons-kit/fa' +import { userMinus } from 'react-icons-kit/feather' + +const { publicRuntimeConfig: { API_URL } } = getConfig() + +// const Icon = styled.div` +// width: 20px; +// height: 20px; +// background-image: url(${(props) => `/static/assets/${props.icon}`}); +// background-size: cover; +// background-repeat: no-repeat; +// display: inline-block; +// position: relative; +// @media(max-width:700px){ +// filter:grayscale(100%) brightness(54%) sepia(100%) hue-rotate(-180deg) saturate(700%) contrast(0.8); +// } +// ` +// const IconLoading = styled(Icon)` +// width:20px; +// height:20px; +// filter:grayscale(100%); +// animation: rotation 2s infinite linear; + +// @keyframes rotation { +// from { +// transform: rotate(0deg); +// } +// to { +// transform: rotate(359deg); +// } +// } +// ` + +const AuthorSubscribeButton = styled.button` + cursor:pointer; + border: none; + padding:9px 20px; + font-size: 1.4rem; + color: ${(props) => props.active ? 'red' : 'green'}; + background-color: ${(props) => props.active ? 'white' : 'white'}; + font-family: ${(props) => !props.active ? 'var(--bold)' : 'var(--regular)'}; + @media(max-width:760px){ + padding:9px; + } +` + +class SubscribeButton extends Component { + constructor (props) { + super(props) + this.state = { + isLoading: true, + isSubscribed: false + } + } + + async componentDidMount () { + // if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // await this.fetchData() + // } + + // when component initiates, this.props.authContext might not be fullfiled, we need to + // set an interval to check if it is already there + const interval = setInterval(() => { + // console.log('Is it loaded?') + if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // console.log('Yes it is') + clearInterval(interval) + this.fetchData() + } else { + // console.log('Not yet') + } + }, 1000) + } + + + async fetchData () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}/check`, { + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + if(data.error) return console.log(data.error) + this.setState({ + isLoading: false, + isSubscribed: data.isSubscribed + }) + } + + + async handleAuthorSubscribeClick () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token; + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + isLoading: false, + isSubscribed: data.added + }) + } + + render () { + if (!this.props.authContext || !this.props.authContext.authenticated) return null + + if(this.props.authContext && this.props.authContext && this.props.authContext.user && this.props.authContext.user._id === this.props.authorId) return null + + if (this.state.isLoading) { + return null + } + + return ( + this.handleAuthorSubscribeClick()} active={this.state.isSubscribed}> + 760 ? 14 : 10} /> {this.state.isSubscribed ? 'Desuscribirse del diputado' : 'Suscribirse al diputado'} + + ) + } +} + +export default WithUserContext(SubscribeButton) diff --git a/components/subscribe-button-profile/component.js b/components/subscribe-button-profile/component.js new file mode 100644 index 00000000..f15da255 --- /dev/null +++ b/components/subscribe-button-profile/component.js @@ -0,0 +1,134 @@ +import React, { Component, Fragment } from 'react' +// import PropTypes from 'prop-types' +import styled from 'styled-components' +import fetch from 'isomorphic-unfetch' +import getConfig from 'next/config' +import WithUserContext from '../with-user-context/component' +import Icon from 'react-icons-kit' +import { userPlus } from 'react-icons-kit/fa' +import { userMinus } from 'react-icons-kit/feather' + +const { publicRuntimeConfig: { API_URL } } = getConfig() + +// const Icon = styled.div` +// width: 20px; +// height: 20px; +// background-image: url(${(props) => `/static/assets/${props.icon}`}); +// background-size: cover; +// background-repeat: no-repeat; +// display: inline-block; +// position: relative; +// @media(max-width:700px){ +// filter:grayscale(100%) brightness(54%) sepia(100%) hue-rotate(-180deg) saturate(700%) contrast(0.8); +// } +// ` +// const IconLoading = styled(Icon)` +// width:20px; +// height:20px; +// filter:grayscale(100%); +// animation: rotation 2s infinite linear; + +// @keyframes rotation { +// from { +// transform: rotate(0deg); +// } +// to { +// transform: rotate(359deg); +// } +// } +// ` + +const AuthorSubscribeButton = styled.div` + cursor:pointer; + border: none; + padding:9px 20px; + font-size: 1.4rem; + color: ${(props) => props.active ? 'red' : 'green'}; + background-color: ${(props) => props.active ? 'white' : 'white'}; + font-family: ${(props) => !props.active ? 'var(--bold)' : 'var(--regular)'}; + @media(max-width:760px){ + padding:9px; + } +` + +class SubscribeButton extends Component { + constructor (props) { + super(props) + this.state = { + isLoading: true, + isSubscribed: false + } + } + + async componentDidMount () { + // if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // await this.fetchData() + // } + + // when component initiates, this.props.authContext might not be fullfiled, we need to + // set an interval to check if it is already there + const interval = setInterval(() => { + // console.log('Is it loaded?') + if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // console.log('Yes it is') + clearInterval(interval) + this.fetchData() + } else { + // console.log('Not yet') + } + }, 1000) + } + + + async fetchData () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}/check`, { + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + if(data.error) return console.log(data.error) + this.setState({ + isLoading: false, + isSubscribed: data.isSubscribed + }) + } + + + async handleAuthorSubscribeClick () { + // prevent propagation to avoid triggering the onClick event of the parent element + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token; + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + isLoading: false, + isSubscribed: data.added + }) + } + + render () { + if (!this.props.authContext || !this.props.authContext.authenticated) return null + + if(this.props.authContext && this.props.authContext && this.props.authContext.user && this.props.authContext.user._id === this.props.authorId) return null + + if (this.state.isLoading) { + return null + } + + return ( + this.handleAuthorSubscribeClick(e)} active={this.state.isSubscribed}> + 760 ? 14 : 10} /> {this.state.isSubscribed ? 'Desuscribirse del diputado' : 'Suscribirse al diputado'} + + ) + } +} + +export default WithUserContext(SubscribeButton) diff --git a/components/subscribe-button/component.js b/components/subscribe-button/component.js new file mode 100644 index 00000000..b1893a03 --- /dev/null +++ b/components/subscribe-button/component.js @@ -0,0 +1,133 @@ +import React, { Component, Fragment } from 'react' +// import PropTypes from 'prop-types' +import styled from 'styled-components' +import fetch from 'isomorphic-unfetch' +import getConfig from 'next/config' +import WithUserContext from '../../components/with-user-context/component' +import Icon from 'react-icons-kit' +import { userPlus } from 'react-icons-kit/fa' +import { userMinus } from 'react-icons-kit/feather' + +const { publicRuntimeConfig: { API_URL } } = getConfig() + +// const Icon = styled.div` +// width: 20px; +// height: 20px; +// background-image: url(${(props) => `/static/assets/${props.icon}`}); +// background-size: cover; +// background-repeat: no-repeat; +// display: inline-block; +// position: relative; +// @media(max-width:700px){ +// filter:grayscale(100%) brightness(54%) sepia(100%) hue-rotate(-180deg) saturate(700%) contrast(0.8); +// } +// ` +// const IconLoading = styled(Icon)` +// width:20px; +// height:20px; +// filter:grayscale(100%); +// animation: rotation 2s infinite linear; + +// @keyframes rotation { +// from { +// transform: rotate(0deg); +// } +// to { +// transform: rotate(359deg); +// } +// } +// ` + +const AuthorSubscribeButton = styled.button` + cursor:pointer; + border: none; + padding:9px 20px; + font-size: 1.4rem; + color: ${(props) => props.active ? 'red' : 'green'}; + background-color: ${(props) => props.active ? 'white' : 'white'}; + font-family: ${(props) => !props.active ? 'var(--bold)' : 'var(--regular)'}; + @media(max-width:760px){ + padding:9px; + } +` + +class SubscribeButton extends Component { + constructor (props) { + super(props) + this.state = { + isLoading: true, + isSubscribed: false + } + } + + async componentDidMount () { + // if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // await this.fetchData() + // } + + // when component initiates, this.props.authContext might not be fullfiled, we need to + // set an interval to check if it is already there + const interval = setInterval(() => { + // console.log('Is it loaded?') + if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // console.log('Yes it is') + clearInterval(interval) + this.fetchData() + } else { + // console.log('Not yet') + } + }, 1000) + } + + + async fetchData () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}/check`, { + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + if(data.error) return console.log(data.error) + this.setState({ + isLoading: false, + isSubscribed: data.isSubscribed + }) + } + + + async handleAuthorSubscribeClick () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token; + const authorId = this.props.authorId + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + isLoading: false, + isSubscribed: data.added + }) + } + + render () { + if (!this.props.authContext || !this.props.authContext.authenticated) return null + + if(this.props.authContext && this.props.authContext && this.props.authContext.user && this.props.authContext.user._id === this.props.authorId) return null + + if (this.state.isLoading) { + return null + } + + return ( + this.handleAuthorSubscribeClick()} active={this.state.isSubscribed}> + 760 ? 14 : 10} /> {this.state.isSubscribed ? 'Desuscribirse del diputado' : 'Suscribirse al diputado'} + + ) + } +} + +export default WithUserContext(SubscribeButton) diff --git a/components/validar-apoyo/component.js b/components/validar-apoyo/component.js index c1336b17..cac7f07a 100644 --- a/components/validar-apoyo/component.js +++ b/components/validar-apoyo/component.js @@ -25,6 +25,7 @@ const ValidateBoxWrapper= styled.div` width:80%; background-color: #fFf; text-align:center +padding: 10px; ` const ApoyoLogo = styled.img` margin:3rem 0 @@ -33,7 +34,7 @@ filter: invert(0.6) sepia(0.4) saturate(5) hue-rotate(175deg); const Box = styled.div` font-size: 1.8rem; - padding: 20px 40px 0 40px; + padding: 10px; p{ font-family: var(--light); :first-of-type{ @@ -47,8 +48,8 @@ const Box = styled.div` const ProjectsTitle = styled.h1` font-weight: 100; font-family: var(--light); - margin-top: 50px; - padding-bottom: 20px; + margin-top: 10px; + margin-bottom: 5px; a{ color:#5c98bd; } @@ -65,19 +66,23 @@ export default () => { 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))) - } + setTimeout(() => { + fetch(`${API_URL}/api/v1/documents/apoyo-anon-validar/${v}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + }, }) + .then(r => r.json()) + .then(j => { + if (j.error){ + setIsValidado(false) + }else { + let rProject = j.document + setIsValidado(true) + } + }) + }, 3000) }, []) return ( @@ -96,25 +101,19 @@ export default () => {
: - 'No se ha podido validar su apoyo' + 'No se ha podido validar su apoyo, o ya ha apoyado el proyecto' ) } - +

- - O vea otros proyectos aquí - {/* {projects && - - {projects.map((p, i) => ( - - ))} - - } */} + { + isValidado && Vea otros proyectos aquí + } diff --git a/containers/general-container/component.js b/containers/general-container/component.js index c4ec450c..7b97c0b4 100644 --- a/containers/general-container/component.js +++ b/containers/general-container/component.js @@ -26,7 +26,8 @@ class GeneralContainer extends Component { componentDidMount () { if (!this.props.authContext.keycloak) return - if (this.props.authContext.user.roles.includes('admin')) {this.setState({isAdmin: true})} + // if (!this.props.authContext.user) return + if (this.props.authContext.authenticated && this.props.authContext.user && this.props.authContext.user.roles.includes('admin')) {this.setState({isAdmin: true})} this.fetchDocument(this.props.project, this.props.authContext.keycloak.token) } diff --git a/containers/my-notifications-settings/component.js b/containers/my-notifications-settings/component.js new file mode 100644 index 00000000..c0b980a4 --- /dev/null +++ b/containers/my-notifications-settings/component.js @@ -0,0 +1,376 @@ +import React, { Component, Fragment } from 'react' +// import PropTypes from 'prop-types' +import styled from 'styled-components' +import fetch from 'isomorphic-unfetch' +import Router from 'next/router' +import Link from 'next/link' +import getConfig from 'next/config' +import Masonry from 'react-masonry-component' +import { clockO } from 'react-icons-kit/fa' +import { plus, download } from 'react-icons-kit/feather' +import { times } from 'react-icons-kit/fa' +import Section from '../section/component' +import Card from '../../components/card/component' +import CardNewProject from '../../components/card-new-project/component' +import WithUserContext from '../../components/with-user-context/component' +import TitleH2 from '../../elements/title-h2/component' +import Alert from '../../elements/alert/component' +import ProjectTableItem from '../../components/project-table-item/component' +// import Icon from 'react-icons-kit' +import WithDocumentTagsContext from '../../components/document-tags-context/component' + +const { publicRuntimeConfig: { API_URL } } = getConfig() + +const Icon = styled.div` + width: 20px; + height: 20px; + background-image: url(${(props) => `/static/assets/${props.icon}`}); + background-size: cover; + background-repeat: no-repeat; + display: inline-block; + position: relative; + @media(max-width:700px){ +filter:grayscale(100%) brightness(54%) sepia(100%) hue-rotate(-180deg) saturate(700%) contrast(0.8); +} +` +const IconLoading = styled(Icon)` +width:20px; +height:20px; +filter:grayscale(100%); +animation: rotation 2s infinite linear; + +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } +} +` + +const TagsWrapper = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: start; + text-align: justify; + margin-bottom: 5px; + margin.top: 5px; +` + +const TagDiv = styled.div` + border: solid 1px #dae1e7; + background-color: #ffffff; + padding: 2px 10px; + border-radius: 5px; + margin-bottom: 5px; + margin-right: 3px; + margin-left: 3px; + cursor: pointer + + &.selected { + background-color: #5c97bc; + border-color: #5c97bc; + color: white; + } +` + +const LoadingContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; +` + +const MyNotificationsWrapper = styled.div` + font-size: 14px; + // border: 1px solid #eaeaea; + // padding: 15px; +` + +const SegmentTitle = styled.div` + font-size: 1.3em; + font-weight: 800; + color: #5c97bc; + margin-bottom: 10px; + margin-top: 15px; + font-family: var(--bold); +` + +const SegmentDescription = styled.div` + font-size: 1em; + margin-bottom: 10px; + font-family: var(--italic); + color: #777; +` + +const AuthorsWrapper = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: start; + text-align: justify; + margin-bottom: 20px; +` + +const AuthorDiv = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-right: 10px; + margin-bottom: 10px; + border: 1px solid #eaeaea; + padding: 10px; +` + +const AuthorAvatar = styled.div` + height: 40px; + width: 40px; + margin-right: 10px; + border-radius: 50%; + border: 1px solid #eaeaea; + background-image: url('${(props) => props.userId ? `${API_URL}/api/v1/users/${props.userId}/avatar` : '/static/assets/userdefault.png'}'); + background-size: cover; + background-position: center; +` + +const AuthorName = styled.div` + font-size: 1em; + font-weight: 800; + font-family: var(--bold); + color: #5c97bc; + margin-bottom: 5px; +` + +const AuthorUnsubscribeButton = styled.button` + background-color: #5c97bc; + font-size: 0.8em; + color: #FFF; + border: none; + padding: 3px 10px; + border-radius: 4px; + cursor: pointer; + &:hover { + background-color: #2c4c61; + } +` + +const TagsNotificationCheckboxDiv = styled.div` + display: flex; + & > input { + margin-right: 7px; + margin-bottom: auto; + } +` + +class MyNotificationsSettings extends Component { + constructor (props) { + super(props) + this.state = { + isLoading: true, + tagsNotification: false, + popularNotification: false, + availableTags: [], + userSubscribedTags: [], + userSubscribedAuthors: [] + } + } + + async componentDidMount () { + // console.log('HELP ME') + // if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // await this.fetchData() + // } + + // when component initiates, this.props.authContext might not be fullfiled, we need to + // set an interval to check if it is already there + const interval = setInterval(() => { + // console.log('Is it loaded?') + if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // console.log('Yes it is') + clearInterval(interval) + this.fetchData() + } else { + // console.log('Not yet') + } + }, 1000) + } + + // async componentWillUpdate (props) { + // console.log('HELP ME but this time component will update') + // if (this.props.authContext && this.props.authContext.user && this.props.authContext.user._id) { + // await this.fetchData() + // } + // } + + async fetchData () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings`, { + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + console.log(data) + this.setState({ + isLoading: false, + availableTags: data.availableDocumentTags, + tagsNotification: data.tagsNotification, + popularNotification: data.popularNotification, + userSubscribedTags: data.userSubscribedTags, + userSubscribedTagsIds: data.userSubscribedTagsIds, + userSubscribedAuthors: data.userSubscribedAuthors + }) + } + + async handleTagClick (tag) { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/tags/${tag}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + userSubscribedTagsIds: data.userTags + }) + } + + async handleAuthorUnsubscribeClick (authorId) { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/authors/${authorId}`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + if(data.added === false) { + // remove the author from the list + this.setState((prevState) => ({ + userSubscribedAuthors: prevState.userSubscribedAuthors.filter((author) => author._id !== authorId) + })) + } + } + + async toggleTagsCheckboxChange () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/tagsNotification`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + tagsNotification: data.tagsNotification + }) + }; + + async togglePopularCheckboxChange () { + const token = this.props.authContext && this.props.authContext.keycloak && this.props.authContext.keycloak.token + const data = await (await fetch(`${API_URL}/api/v1/users/notifications/settings/popularNotification`, { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + } + })).json() + this.setState({ + popularNotification: data.popularNotification + }) + }; + + render () { + if (!this.props.authContext || !this.props.authContext.authenticated) return null + + + if (this.props.userId && this.props.userId !== this.props.authContext.user._id) { + return null + } + + if (this.state.isLoading) { + return ( + +
+ Mis notificaciones + + + +  Cargando + +
+
+ ) + } + + return ( + +
+ Mis notificaciones +
+ Suscripción a etiquetas + + this.toggleTagsCheckboxChange()} + />Deseo recibir notificaciones de proyectos de las etiquetas que me interesan. + + Etiquetas disponibles + Seleccione las etiquetas a las que desea suscribirse para recibir notificaciones de nuevos proyectos asociados a las mismas + + { + this.state.availableTags.map((tag) => ( + this.handleTagClick(tag._id)}> + {tag.name} + + )) + } + +
+ Suscripción a proyectos populares + + this.togglePopularCheckboxChange()} + />Haga clic en el checkbox para recibir notificaciones cada vez que un proyecto se vuelva popular + + Suscripción a autores de propuestas + Puede suscribirse a un autor ingresando a una propuesta o al perfil del mismo. La siguiente lista muestra aquellos a los que se ha suscripto. Para desuscribirse a las notificaciones, haga clic en el botón "Desuscribirse". + + { + this.state.userSubscribedAuthors.map((author) => ( + + +
+ {author.name} + this.handleAuthorUnsubscribeClick(author._id)}> + Desuscribirse + +
+
+ )) + } + { + this.state.userSubscribedAuthors.length === 0 && + +      × No se ha suscripto a ningún autor + + } +
+
+
+
+
+ ) + } +} + +export default WithUserContext(MyNotificationsSettings) diff --git a/containers/my-projects/component.js b/containers/my-projects/component.js index 6eaf4536..8f7968c9 100644 --- a/containers/my-projects/component.js +++ b/containers/my-projects/component.js @@ -23,182 +23,6 @@ const masonryOptions = { transitionDuration: 0 }; - -const getClosingDate = () => { - let closingDate = new Date() - closingDate.setDate(closingDate.getDate() + 30) - closingDate.setHours(0, 0, 0, 0) - return closingDate.toISOString() -} - -const newDocument = { - 'published': false, - 'closed': false, - 'customForm': 'project-form', - 'content': { - 'title': 'Mi nuevo proyecto', - 'imageCover': null, - 'youtubeId': null, - 'customVideoId': null, - 'sendTagsNotification': true, - 'fundation': { - 'object': 'value', - 'document': { - 'object': 'document', - 'data': { - }, - 'nodes': [ - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Esta sección es un espacio para redactar un texto que sirve para presentar el proyecto, explicar el contexto (de donde surge, su importancia, etc.), e invitar la ciudadanía a participar. Es muy importante mencionar qué tipo de aportes ciudadanos se esperan. El proyecto tiene que estar explicado de manera muy simple, la redacción debe ser fácil de entender.', - 'marks': [ - ] - } - ] - } - ] - } - ] - } - }, - 'articles': { - 'object': 'value', - 'document': { - 'object': 'document', - 'data': { - }, - 'nodes': [ - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 1º.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sed purus justo. Nam tempus ligula nec est scelerisque aliquet. Phasellus pretium rhoncus pharetra. Duis dapibus felis neque.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 2°.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Fusce elementum posuere dolor id mattis. Sed magna arcu, rutrum eu pellentesque nec, feugiat sit amet lorem. Fusce volutpat, dolor a pretium fermentum, felis justo rhoncus nisl, vel mollis est diam mollis nisl. Sed aliquet erat sed ipsum lacinia, feugiat interdum ante pulvinar. Integer ut consectetur velit.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'title', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'Art. 3°.', - 'marks': [ - ] - } - ] - } - ] - }, - { - 'object': 'block', - 'type': 'paragraph', - 'data': { - }, - 'nodes': [ - { - 'object': 'text', - 'leaves': [ - { - 'object': 'leaf', - 'text': 'In id neque posuere, dictum arcu vitae, euismod nulla. Integer eu euismod ipsum. In aliquet nisl mi, nec vulputate urna hendrerit eu. Integer in mi at quam luctus posuere. Integer magna sem, viverra non ultrices vitae, varius in mi.', - 'marks': [ - ] - } - ] - } - ] - } - ] - } - }, - 'closure': null, - 'closingDate': getClosingDate() - } -} - const ProjectsTable = styled.table` width: 100%; margin: 20px 0; @@ -487,7 +311,8 @@ class MyProjects extends Component { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.props.authContext.keycloak.token }, - 'body': JSON.stringify(newDocument) + // no more body, now the document base is in the backend + // 'body': JSON.stringify(newDocument) }) .then((res) => { if (!res.ok) { diff --git a/elements/card-content/component.js b/elements/card-content/component.js index da40d98c..387e4139 100644 --- a/elements/card-content/component.js +++ b/elements/card-content/component.js @@ -16,7 +16,7 @@ const Tags = styled.div` width:90%; margin: auto margin-bottom:0; - padding:20px 0 0 0 + padding: 10px 0 0 0 display: flex; flex-direction:row; flex-wrap: wrap; diff --git a/elements/progress-bar/component.js b/elements/progress-bar/component.js index 54f4750e..9d6feeb0 100644 --- a/elements/progress-bar/component.js +++ b/elements/progress-bar/component.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import styled from 'styled-components' const ProgressBarWrapper = styled.div` - margin-top: 20px; + margin-top: 4px; background-color: #c6c8ca; overflow: hidden; height: 40px; diff --git a/elements/project-tag/component.js b/elements/project-tag/component.js index 5f0b8c44..13597743 100644 --- a/elements/project-tag/component.js +++ b/elements/project-tag/component.js @@ -8,11 +8,11 @@ color: #4C4C4E; border-radius:5px; font-weight: 600; font-family: var(--italic); -padding:8px; -font-size:13px -line-height: 15px; +padding:4px 6px; +font-size:12px +line-height: 14px; text-align: center; -letter-spacing: 1.1px; +// letter-spacing: 1.1px; text-transform: capitalize; ` export default ProjectTag \ No newline at end of file diff --git a/elements/tag-form/component.js b/elements/tag-form/component.js index 1df542a3..3663ecbf 100644 --- a/elements/tag-form/component.js +++ b/elements/tag-form/component.js @@ -1,8 +1,8 @@ import React, { Component, useEffect, useState } from 'react' import styled from 'styled-components' import Icon from 'react-icons-kit' -import {trash2} from 'react-icons-kit/feather' - +import { trash2 } from 'react-icons-kit/feather' +import slugify from 'slugify' import PropTypes from 'prop-types' const NewTagWrapper = styled.div` @@ -32,8 +32,8 @@ width: 100%; const TagButton = styled.button` margin:23px 0; -min-width: 125px; -max-width: 230px; +min-width: 100px; +max-width: 200px; height: 39px; background-color: #5c97bc; font-size: 1.4rem; @@ -42,29 +42,32 @@ border-style: none; cursor: pointer; padding: 0 2rem; font-family: var(--bold); +&[disabled] { + background-color: #CACACA; + color: #FFF; + cursor: not-allowed; + } ` const TagNew = (props) => { - const [input, setInput] = useState('') - const handleInput=(e)=>{ - setInput(e.target.value) - } - const sendTag = ()=>{ - props.addTag(input) - setInput('') - } - return( + const [input, setInput] = useState('') + const handleInput = (e) => { + setInput(e.target.value) + } + const sendTag = () => { + props.addTag(input) + setInput('') + } + return ( - Agregar nueva etiqueta + Agregar nueva etiqueta + handleInput(e)} /> + sendTag()} disabled={!input}>Agregar + + - handleInput(e)}/> - sendTag()}> agregar - - - - - - )} + ) +} TagNew.propTypes = { } diff --git a/elements/tag-list/component.js b/elements/tag-list/component.js index 12214d28..4edf5394 100644 --- a/elements/tag-list/component.js +++ b/elements/tag-list/component.js @@ -20,11 +20,10 @@ color: #4C4C4E; border-radius:5px; font-weight: 600; font-family: var(--italic); -padding:8px; -font-size:12px +padding: 5px 10px; +font-size: 12px line-height: 15px; text-align: center; -letter-spacing: 1.1px; text-transform: capitalize; ` diff --git a/package-lock.json b/package-lock.json index d9e81d0a..f6d696bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "leyesabiertas-web", - "version": "2.1.0", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "leyesabiertas-web", - "version": "2.1.0", + "version": "3.0.0", "license": "ISC", "dependencies": { "@react-hook/media-query": "^1.1.1", @@ -32,6 +32,7 @@ "react-select": "^3.1.0", "slate": "0.43.0", "slate-react": "0.20.1", + "slugify": "^1.6.6", "styled-components": "^3.4.10", "video.js": "^7.6.5" }, @@ -9194,6 +9195,14 @@ "node": ">=6" } }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -18782,6 +18791,11 @@ "is-fullwidth-code-point": "^2.0.0" } }, + "slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==" + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", diff --git a/package.json b/package.json index 17015280..0726c233 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "leyesabiertas-web", - "version": "2.1.0", + "version": "3.0.0", "description": "", "main": "index.js", "scripts": { @@ -42,6 +42,7 @@ "react-select": "^3.1.0", "slate": "0.43.0", "slate-react": "0.20.1", + "slugify": "^1.6.6", "styled-components": "^3.4.10", "video.js": "^7.6.5" }, diff --git a/pages/userprofile.js b/pages/userprofile.js index 8386fe76..5d7c39b8 100644 --- a/pages/userprofile.js +++ b/pages/userprofile.js @@ -5,6 +5,7 @@ import NavBar from '../containers/navbar/component' import SecondaryNavbar from '../containers/secondary-navbar/component' import UserProfileContainer from '../containers/user-profile/component' import MyProjects from '../containers/my-projects/component' +import MyNotificationsSettings from '../containers/my-notifications-settings/component' import Footer from '../containers/footer/component' const Wrapper = styled.div` @@ -26,6 +27,7 @@ class UserProfile extends Component { {/* */} +