Pages (terms/tos/privacy), comments improvements, etc

This commit is contained in:
vjrj 2018-01-21 20:53:21 +01:00
parent cd16f45d8e
commit 952431d296
34 changed files with 435 additions and 97 deletions

View file

@ -14,6 +14,7 @@ const firesCommonSchema = {
owner: { type: String, optional: true },
dateformat: { type: String, optional: true },
ourversion: { type: String, optional: true },
// NASA types
address: { type: String, optional: true }, // reverse geo

View file

@ -0,0 +1,6 @@
import moment from 'moment-timezone';
export const momentTz = date => moment.tz(date, moment.tz.guess());
export const dateLongFormat = date => momentTz(date).format('LLLL (z)');
export const dateParseShortFormat = date => moment(date, 'YYYY-MM-DD').format('LL');
export const dateFromNow = date => momentTz(date).fromNow();

View file

@ -30,6 +30,7 @@ Notifications.schema = new SimpleSchema({
emailNotified: { type: Boolean, optional: true },
emailNotifiedAt: { type: Date, optional: true },
when: Date,
sealed: String,
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
});

View file

@ -1,13 +1,35 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import i18n from 'i18next';
import fs from 'fs';
import getPrivateFile from '../../../modules/server/get-private-file';
import parseMarkdown from '../../../modules/parse-markdown';
const as = f => `assets/app/${f}`;
const getFallback = (lang) => {
if (lang === 'ast' || lang === 'gl' || lang === 'eu' || lang === 'ca') {
return 'es';
}
return 'en';
};
Meteor.methods({
'utility.getPage': function utilityGetPage(fileName) {
'utility.getPage': function utilityGetPage(fileName, lang) {
check(fileName, String);
return parseMarkdown(getPrivateFile(`pages/${fileName}.md`));
check(lang, String);
const base = `pages/${fileName}`;
const fallback = getFallback(lang);
let file = `${base}-${lang}.md`;
if (!fs.existsSync(as(file))) {
console.log(`Page '${fileName}' not found for '${lang}' lang`);
file = `${base}-${fallback}.md`;
if (!fs.existsSync(as(file))) {
console.log(`Page '${fileName}' not found for '${fallback}' lang`);
file = `${base}.md`;
}
}
return parseMarkdown(getPrivateFile(file));
},
'utility.saveMissingI18n': function saveMissing(key, value) {
check(key, String);

View file

@ -28,12 +28,12 @@ i18n.init((err, t) => {
Comments.ui.config({
limit: 20, // default 10
loadMoreCount: 20, // default 20
generateAvatar: function genAvatar(user, isAnonymous) {
/* generateAvatar: function genAvatar(user, isAnonymous) {
if (isAnonymous) {
return i18n.t('Anónimo');
}
return user.profile && user.profile.name && user.profile.name.first ? user.profile.name.first : null;
},
}, */
template: 'bootstrap', // default 'semantic-ui'
// default 'http://s3.amazonaws.com/37assets/svn/765-default-avatar.png'
defaultAvatar: '/default-avatar.png',

View file

@ -3,6 +3,7 @@
import i18n from 'i18next';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { dateLongFormat } from '/imports/api/Common/dates';
import Notifications from '/imports/api/Notifications/Notifications';
import sendMail from '/imports/startup/server/email';
import { hr } from '/imports/startup/server/email';
@ -58,19 +59,28 @@ Meteor.startup(() => {
user && user.emails[0] && user.emails[0].verified ? user.emails[0].address : null;
if (emailAddress) {
const img = imgEl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
// const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]);
const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`;
const fireHtmlUrl = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
// TODO get _id of fire
const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`;
// FIXME use our map as url and static map as img
moment.locale(user.lang);
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: moment(notif.when).format('LLL') })}).`;
// moment user tz ?
const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`;
// TODO unsubscribe link
// TODO Address
const emailOpts = {
to: emailAddress,
userName: firstName,
sendAt: new Date(),
subject: truncate.apply(message, [50, true]),
text: `${message} ${url}`,
text: `${message}\n\n${fireTextUrl}\n\n`,
template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
appName: i18n.t('AppName'),
html: `<p>${message}</p>${hr}<p>${img}</p>`
html: `<p>${message}</p><p>${fireHtmlUrl}</p>${hr}<p>${img}</p>`
};
sendMail(emailOpts, true);
Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } });

View file

@ -2,7 +2,7 @@
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { Bert } from 'meteor/themeteorchef:bert';
import moment from 'moment';
import { dateFromNow } from '/imports/api/Common/dates';
import Notifications from '/imports/api/Notifications/Notifications';
import Push from 'push.js/bin/push.min.js';
import i18n from '/imports/startup/client/i18n';
@ -14,13 +14,13 @@ function process(notif) {
if (Push.Permission.has()) {
if (!notif.webNotified) {
Push.create(i18n.t('AppName'), {
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: moment(notif.when).fromNow() })})`,
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`,
icon: '/n-fire-marker.png',
requireInteraction: true,
onClick: function onClickFocus() {
window.focus();
this.close();
history.push('/fires');
history.push(`/fire/${notif.sealed}`);
}
});

View file

@ -160,8 +160,7 @@ class SelectionMap extends Component {
render() {
const { t, onRemove } = this.props;
return (
<div>
{ this.isValidState() &&
this.isValidState() ?
<Fragment>
<Map
className="selectionmap-leaflet-container"
@ -251,10 +250,8 @@ class SelectionMap extends Component {
</ButtonGroup>
</Control>
</Map>
</Fragment>
}
</div>
);
</Fragment> :
<div />);
}
}

View file

@ -1,12 +1,16 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React from 'react';
import Page from '../Page/Page';
import { translate } from 'react-i18next';
import Page from '../Page/Page';
const Credits = props => (
<div className="Credits">
<Page
title={props.t("Créditos")}
subtitle={props.t("Sobre los datos y imágenes usados")}
title={props.t('Créditos')}
subtitle={props.t('Sobre los datos y imágenes usados')}
page="credits"
/>
</div>

View file

@ -8,12 +8,12 @@ import { withTracker } from 'meteor/react-meteor-data';
import { translate } from 'react-i18next';
import { Meteor } from 'meteor/meteor';
import { Map, Circle } from 'react-leaflet';
import moment from 'moment-timezone';
import Blaze from 'meteor/gadicc:blaze-react-component';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import NotFound from '/imports/ui/pages/NotFound/NotFound';
import FiresCollection from '/imports/api/Fires/Fires';
import { dateLongFormat, dateFromNow } from '/imports/api/Common/dates';
import '/imports/startup/client/comments';
import './Fires.scss';
class Fire extends React.Component {
@ -27,16 +27,17 @@ class Fire extends React.Component {
render() {
const { loading, fire, t } = this.props;
if (fire && fire.when) {
this.when = moment.tz(fire.when, moment.tz.guess());
this.dateLongFormat = dateLongFormat(fire.when);
this.dateFromNow = dateFromNow(fire.when);
}
return (
<div className="ViewFire">
return (fire ?
(<div className="ViewFire">
{!loading &&
<Fragment>
<h4 className="page-header">
{fire.address ?
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.when.format('LLLL (z)') }) :
t('Información adicional sobre fuego detectado el {{when}}', { when: this.when.format('LLLL (z)') })}
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) :
t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat })}
</h4>
<Map
@ -62,10 +63,10 @@ class Fire extends React.Component {
</Map>
<p>{t('Coordenadas:')} {fire.lat}, {fire.lon}</p>
{(fire.type === 'modis' || fire.type === 'viirs') &&
<p>{t('Fuego detectado por satélites de la NASA {{when}}', { when: this.when.fromNow() })}</p>
<p>{t('Fuego detectado por satélites de la NASA {{when}}', { when: this.dateFromNow })}</p>
}
{(fire.type === 'vecinal') &&
<p>{t('Fuego notificado por uno de nuestros usuarios/as {{when}}', { when: this.when.fromNow() })}</p>
<p>{t('Fuego notificado por uno de nuestros usuarios/as {{when}}', { when: this.dateFromNow })}</p>
}
{/* TODO: marcar tipo de fuego, industria, etc */}
<h4>{t('Comentarios')}</h4>
@ -85,8 +86,8 @@ class Fire extends React.Component {
</div>
</Fragment>
}
</div>
);
</div>
) : <NotFound />);
}
}

View file

@ -9,7 +9,7 @@
width: 100%;
/* min-width: 75vw; */
display: flex;
margin: 20px auto;
margin: 10px auto;
}
@include breakpoint(mobile) {

View file

@ -166,6 +166,7 @@
position: relative;
width: 100vw;
margin-left: calc(-50vw + 50%);
margin-top: -21px;
position: relative;
/*
width: 100vw;
@ -212,4 +213,8 @@
.device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
background-image: url(/mobile.png);
}
.full-width .page-header { // section titles
margin-top: 20px;
}

View file

@ -208,7 +208,7 @@ class Index extends Component {
</div>
<div className="col-lg-6">
<div className="feature-item">
<i className="icon-envelope-open text-primary" />
<i className="icon-speech text-primary" />
<h3><Trans>Notificaciones</Trans></h3>
<p className="text-muted"><Trans>Recibe nuestras notificaciones de fuegos por correo o en tu navegador</Trans></p>
</div>

View file

@ -1,12 +1,17 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React from 'react';
import Page from '../Page/Page';
import { translate } from 'react-i18next';
import { dateParseShortFormat } from '/imports/api/Common/dates';
import Page from '../Page/Page';
const License = props => (
<div className="License">
<Page
title={props.t("Licencia")}
subtitle={props.t("Última actualización 15 de noviembre de 2017")}
title={props.t('Licencia')}
subtitle={props.t('Última actualización, {{when}}', { when: dateParseShortFormat('2017-01-19') })}
page="license"
/>
</div>

View file

@ -1,12 +1,15 @@
import React from 'react';
import { Alert } from 'react-bootstrap';
import { translate, Trans } from 'react-i18next';
const NotFound = () => (
<div className="NotFound">
<Alert bsStyle="danger">
<p><strong>Error [404]</strong>: {window.location.pathname} does not exist.</p>
<p>
<Trans i18nKey="not-found">Upppps: Esta página no existe</Trans>
</p>
</Alert>
</div>
);
export default NotFound;
export default translate([], { wait: true })(NotFound);

View file

@ -5,6 +5,7 @@ import { createContainer } from 'meteor/react-meteor-data';
import { ReactiveVar } from 'meteor/reactive-var';
import PageHeader from '../../components/PageHeader/PageHeader';
import Content from '../../components/Content/Content';
import i18n from '/imports/startup/client/i18n';
import './Page.scss';
@ -16,13 +17,13 @@ const Page = ({ title, subtitle, content }) => (
);
Page.defaultProps = {
subtitle: '',
subtitle: ''
};
Page.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
content: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
};
const pageContent = new ReactiveVar('');
@ -30,7 +31,7 @@ const pageContent = new ReactiveVar('');
export default createContainer(({ content, page }) => {
window.scrollTo(0, 0); // Force window to top of page.
Meteor.call('utility.getPage', page, (error, response) => {
Meteor.call('utility.getPage', page, i18n.language, (error, response) => {
if (error) {
console.warn(error);
} else {
@ -39,6 +40,6 @@ export default createContainer(({ content, page }) => {
});
return {
content: content || pageContent.get(),
content: content || pageContent.get()
};
}, Page);

View file

@ -1,12 +1,17 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React from 'react';
import Page from '../Page/Page';
import { translate } from 'react-i18next';
import { dateParseShortFormat } from '/imports/api/Common/dates';
import Page from '../Page/Page';
const Privacy = props => (
<div className="Privacy">
<Page
title={props.t("Política de Privacidad")}
subtitle={props.t("Última actualización 15 de noviembre de 2017")}
title={props.t('Política de Privacidad')}
subtitle={props.t('Última actualización, {{when}}', { when: dateParseShortFormat('2017-01-19') })}
page="privacy"
/>
</div>

View file

@ -1,12 +1,17 @@
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
import React from 'react';
import Page from '../Page/Page';
import { translate } from 'react-i18next';
import { dateParseShortFormat } from '/imports/api/Common/dates';
import Page from '../Page/Page';
const Terms = props => (
<div className="Terms">
<Page
title={props.t("Términos de Servicio")}
subtitle={props.t("Última actualización 15 de noviembre de 2017")}
title={props.t('Términos de Servicio')}
subtitle={props.t('Última actualización, {{when}}', { when: dateParseShortFormat('2017-01-19') })}
page="terms"
/>
</div>

View file

@ -2,7 +2,7 @@
@import './colors';
a, a:hover {
color: $todos-palette5;
color: $todos-palette2;
}
.bg-dark {
@ -19,6 +19,10 @@ h4.page-header {
}
}
.page-header {
margin-top: 25px;
.App > .container { // Because of fixed header, previously page-header {
margin-top: 30px;
}
h4.page-header {
font-size: 2em;
}

View file

@ -49,10 +49,15 @@ p {
margin-bottom: 20px; }
div.section.platf > div.container {
padding: 70px 0; }
padding: 30px 0; }
div.section h2 {
font-size: 50px; }
@include breakpoint(mobile) {
div.section h2 {
font-size: 40px; }
}
div.section.crowd > div.container {
position: relative;
padding: 120px 0; }
@ -84,6 +89,17 @@ div.section.platf .feature-item {
max-width: 325px;
margin: 0 auto; }
@include breakpoint(mobile) {
div.section.platf .device-container,
div.section.platf .feature-item {
margin: 10px auto;
}
.device-wrapper {
margin: 10px auto;
}
}
div.section.platf .device-container {
margin-bottom: 100px; }
@media (min-width: 992px) {
@ -94,6 +110,13 @@ div.section.platf .feature-item {
padding-top: 50px;
padding-bottom: 50px;
text-align: center; }
@include breakpoint(mobile) {
div.section.platf .feature-item {
padding-top: 15px;
padding-bottom: 15px; }
}
div.section.platf .feature-item h3 {
font-size: 30px; }
div.section.platf .feature-item i {