diff --git a/.meteor/packages b/.meteor/packages
index 10ae5fc..ee82606 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -60,4 +60,3 @@ mongo-livedata
reywood:publish-composite
barbatus:stars-rating
arkham:comments-ui
-remcoder:chronos
diff --git a/.meteor/versions b/.meteor/versions
index adc9c37..8a03c20 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -111,7 +111,6 @@ react-meteor-data@0.2.15
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.1.11
-remcoder:chronos@0.5.0
retry@1.0.9
reywood:publish-composite@1.5.2
routepolicy@1.0.12
@@ -138,4 +137,3 @@ url@1.1.0
vjrj:piwik@0.3.1
webapp@1.4.0
webapp-hashing@1.0.9
-zodiase:function-bind@0.0.1
diff --git a/imports/api/SiteSettings/SiteSettings.js b/imports/api/SiteSettings/SiteSettings.js
new file mode 100644
index 0000000..e690671
--- /dev/null
+++ b/imports/api/SiteSettings/SiteSettings.js
@@ -0,0 +1,49 @@
+/* eslint-disable consistent-return */
+/* eslint-disable import/no-absolute-path */
+
+import { Mongo } from 'meteor/mongo';
+import SimpleSchema from 'simpl-schema';
+import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
+import SiteSettingsTypes from './SiteSettingsTypes';
+
+const SiteSettings = new Mongo.Collection('siteSettings', { idGeneration: 'MONGO' });
+
+SiteSettings.allow({
+ insert: () => false,
+ update: () => false,
+ remove: () => false
+});
+
+SiteSettings.deny({
+ insert: () => true,
+ update: () => true,
+ remove: () => true
+});
+
+SiteSettings.get = (name) => {
+ const setting = SiteSettings.findOne({
+ name
+ });
+ return typeof setting === 'object' ? setting.value : undefined;
+};
+
+SiteSettings.observe = (name, callback) => {
+ SiteSettings.find({ name }).observe({
+ added: (document) => {
+ callback(document.value);
+ }
+ });
+};
+
+SiteSettings.getSchema = type => new SimpleSchema({
+ name: String,
+ type: String,
+ description: String,
+ value: SiteSettingsTypes[type].value,
+ createdAt: defaultCreatedAt,
+ updatedAt: defaultUpdateAt
+});
+
+// SiteSettings.attachSchema(SiteSettings.schema);
+
+export default SiteSettings;
diff --git a/imports/api/SiteSettings/SiteSettingsTypes.js b/imports/api/SiteSettings/SiteSettingsTypes.js
new file mode 100644
index 0000000..bff3c00
--- /dev/null
+++ b/imports/api/SiteSettings/SiteSettingsTypes.js
@@ -0,0 +1,66 @@
+// Adapted from: https://github.com/yogiben/meteor-admin-settings
+
+const SiteSettingsTypes = {
+ string: {
+ value: {
+ type: String
+ }
+ },
+ textarea: {
+ value: {
+ type: String
+ /* autoform: {
+ * afFieldInput: {
+ * rows: 5
+ * }
+ * } */
+ }
+ },
+ number: {
+ value: {
+ type: Number
+ }
+ },
+ boolean: {
+ value: {
+ type: Boolean
+ /* autoform: {
+ * afFieldInput: {
+ * type: 'boolean-select'
+ * }
+ * } */
+ }
+ },
+ date: {
+ value: {
+ type: Date
+ /* autoform: {
+ * afFieldInput: {
+ * type: 'date'
+ * }
+ * } */
+ }
+ },
+ color: {
+ value: {
+ type: String
+ /* autoform: {
+ * afFieldInput: {
+ * type: 'color'
+ * }
+ * } */
+ }
+ },
+ password: {
+ value: {
+ type: String
+ /* autoform: {
+ * afFieldInput: {
+ * type: 'password'
+ * }
+ * } */
+ }
+ }
+};
+
+export default SiteSettingsTypes;
diff --git a/imports/api/SiteSettings/methods.js b/imports/api/SiteSettings/methods.js
new file mode 100644
index 0000000..737bc3a
--- /dev/null
+++ b/imports/api/SiteSettings/methods.js
@@ -0,0 +1,60 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import SiteSettings from './SiteSettings';
+import SiteSettingsTypes from './SiteSettingsTypes';
+import rateLimit from '../../modules/rate-limit';
+
+Meteor.methods({
+ 'siteSettings.insert': function siteSettingsInsert(setting) {
+ check(setting, {
+ name: String,
+ type: String,
+ description: String,
+ value: SiteSettingsTypes[self.type].value
+ });
+
+ SiteSettings.getSchema.validate(setting);
+
+ try {
+ return SiteSettings.insert({ owner: this.userId, ...setting });
+ } catch (exception) {
+ throw new Meteor.Error('500', exception);
+ }
+ },
+ 'siteSettings.update': function siteSettingsUpdate(setting) {
+ check(setting, {
+ _id: String,
+ name: String,
+ type: String,
+ description: String,
+ value: SiteSettingsTypes[self.type].value
+ });
+
+ try {
+ const siteSettingId = setting._id;
+ SiteSettings.update(siteSettingId, { $set: setting });
+ return siteSettingId; // Return _id so we can redirect to siteSetting after update.
+ } catch (exception) {
+ throw new Meteor.Error('500', exception);
+ }
+ },
+ 'siteSettings.remove': function siteSettingsRemove(siteSettingId) {
+ check(siteSettingId, String);
+
+ try {
+ return SiteSettings.remove(siteSettingId);
+ } catch (exception) {
+ throw new Meteor.Error('500', exception);
+ }
+ }
+});
+
+rateLimit({
+ methods: [
+ 'siteSettings.insert',
+ 'siteSettings.update',
+ 'siteSettings.remove'
+ ],
+ limit: 5,
+ timeRange: 1000
+});
diff --git a/imports/api/SiteSettings/server/publications.js b/imports/api/SiteSettings/server/publications.js
new file mode 100644
index 0000000..cd7f33d
--- /dev/null
+++ b/imports/api/SiteSettings/server/publications.js
@@ -0,0 +1,6 @@
+/* eslint-disable prefer-arrow-callback */
+
+import { Meteor } from 'meteor/meteor';
+import SiteSettings from '../SiteSettings';
+
+Meteor.publish('settings', () => SiteSettings.find());
diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js
index bfb830e..64b3e9f 100644
--- a/imports/startup/server/api.js
+++ b/imports/startup/server/api.js
@@ -21,3 +21,6 @@ import '../../api/Notifications/server/publications';
import '../../api/Fires/methods';
import '../../api/Fires/server/publications';
+
+import '../../api/SiteSettings/methods';
+import '../../api/SiteSettings/server/publications';
diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js
index d8ecff4..1c49a4c 100644
--- a/imports/startup/server/migrations.js
+++ b/imports/startup/server/migrations.js
@@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base';
import randomHex from 'crypto-random-hex';
import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
+import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
Meteor.startup(() => {
// https://github.com/percolatestudio/meteor-migrations
@@ -83,8 +84,16 @@ Meteor.startup(() => {
}
});
+ Migrations.add({
+ version: 5,
+ up: function siteSettingsIndex() {
+ // other way:
+ SiteSettings._ensureIndex({ name: 1 }, { unique: 1 });
+ }
+ });
+
// Set createdAt in users & subs
Migrations.migrateTo('latest');
- // Migrations.migrateTo('2,rerun');
+ // Migrations.migrateTo('5,rerun');
});
diff --git a/imports/ui/components/Chronos/Chronos.js b/imports/ui/components/Chronos/Chronos.js
new file mode 100644
index 0000000..6a96c31
--- /dev/null
+++ b/imports/ui/components/Chronos/Chronos.js
@@ -0,0 +1,122 @@
+// Adapted from: https://github.com/remcoder/chronos (MIT License)
+
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+import { ReactiveVar } from 'meteor/reactive-var';
+import moment from 'moment';
+
+const _timers = {};
+
+function Timer(interval) {
+ this.interval = interval || 1000;
+ this.time = new ReactiveVar(0);
+}
+
+Timer.prototype.start = function () {
+ if (this._timer) throw new Error('Trying to start Chronos.Timer but it is already running.');
+ this.time.set(new Date());
+
+
+ this._timer = setInterval(Meteor.bindEnvironment(() => {
+ // console.log('tick', this._timer);
+ this.time.set(new Date());
+ }), this.interval);
+};
+
+Timer.prototype.stop = function () {
+ // console.log('stopping timer');
+ clearInterval(this._timer);
+ this._timer = null;
+};
+
+function _update(interval) {
+ // get current reactive context
+ const comp = Tracker.currentComputation;
+ if (!comp) { return; } // no nothing when used outside a reactive context
+
+ // only create one timer per reactive context to prevent stacking of timers
+ const cid = comp && comp._id;
+ if (!_timers[cid]) {
+ const timer = new Timer(interval);
+ _timers[cid] = timer;
+
+ // add destroy method that stops the timer and removes itself from the list
+ timer.destroy = function () {
+ timer.stop();
+ delete _timers[cid];
+ };
+
+ timer.start();
+ }
+
+ // make sure to stop and delete the attached timer when the computation is stopped
+ comp.onInvalidate(() => {
+ // console.log('onInvalidated',comp);
+ if (comp.stopped && _timers[cid]) {
+ // console.log('computation stopped');
+ _timers[cid].destroy();
+ }
+ });
+
+ _timers[cid].time.dep.depend(comp); // make dependent on time
+
+ // console.log(_timers);
+ return _timers[cid];
+}
+
+// reactive version of moment()
+// please install moment separately
+// example usage: Chronos.moment(someTimestamp).format('ss');
+function _moment(/* arguments */) {
+ if (!moment) throw new Error('moment not found. Please install it first');
+
+ _update();
+ return moment(...arguments);
+}
+
+// reactive version of new Date() get the current date/time
+function _date(interval) {
+ _update(interval);
+ return new Date();
+}
+
+// reactive version of Date.now(). get the current # of milliseconds since the start of the epoch
+function _now(interval) {
+ _update(interval);
+ return Date.now();
+}
+
+// export global
+const Chronos = {
+
+ // a simple reactive timer
+ // usage: var timer = new Timer();
+ // get current time: timer.time.get();
+ Timer,
+
+ // handy util func for making reactive contexts live updating in time
+ // usage: simply call Chronos.update() in your helper to make it execute
+ // every interval
+ update: _update,
+
+ // reactive version of new Date() get the current date/time
+ date: _date,
+
+ // reactive version of Date.now(). get the current # of milliseconds since the start of the epoch
+ now: _now,
+
+ // reactive version of moment()
+ // please install moment separately
+ // example usage: Chronos.moment(someTimestamp).format('ss');
+ moment: _moment,
+
+ // for debugging and testing
+ _timers,
+
+ // deprecated. but kept for backwards compatibility
+ liveUpdate: _update,
+ currentTime: _date,
+ liveMoment: _moment
+};
+
+export default Chronos;
diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js
new file mode 100644
index 0000000..b540097
--- /dev/null
+++ b/imports/ui/components/FromNow/FromNow.js
@@ -0,0 +1,52 @@
+/* eslint-disable react/jsx-indent-props */
+/* eslint-disable import/no-absolute-path */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withTracker } from 'meteor/react-meteor-data';
+import Chronos from '/imports/ui/components/Chronos/Chronos';
+
+class FromNow extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ when: props.when
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.when !== nextProps.when) {
+ // console.log(`Next when ${nextProps.when}`);
+ this.setState({
+ when: nextProps.when
+ });
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return !(nextState.when === this.state.when);
+ }
+
+ render() {
+ console.log(`Render from now ${this.state.when}`);
+ return (
+ {this.state.when}
+ );
+ }
+}
+
+FromNow.propTypes = {
+ when: PropTypes.string
+};
+
+FromNow.defaultProps = {
+};
+
+const FromNowContainer = withTracker((props) => {
+ const whenDate = props.when;
+ return {
+ when: whenDate ? Chronos.moment(whenDate).fromNow() : null
+ };
+})(FromNow);
+
+export default FromNowContainer;
diff --git a/imports/ui/pages/Fires/Fires.js b/imports/ui/pages/Fires/Fires.js
index 3d45204..38c7aad 100644
--- a/imports/ui/pages/Fires/Fires.js
+++ b/imports/ui/pages/Fires/Fires.js
@@ -1,4 +1,3 @@
-/* global Chronos */
/* eslint-disable import/no-absolute-path */
/* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */
@@ -6,13 +5,14 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
-import { translate } from 'react-i18next';
+import { translate, Trans } from 'react-i18next';
import { Meteor } from 'meteor/meteor';
import { Map, Circle } from 'react-leaflet';
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 FromNow from '/imports/ui/components/FromNow/FromNow';
import { dateLongFormat } from '/imports/api/Common/dates';
import '/imports/startup/client/comments';
import './Fires.scss';
@@ -40,10 +40,9 @@ class Fire extends React.Component {
render() {
const { loading, fire, t } = this.props;
- console.log(`Render ${this.state.when}`);
if (fire && fire.when) {
this.dateLongFormat = dateLongFormat(fire.when);
- this.dateFromNow = this.state.when;
+ // this.dateFromNow = () => (
{t('Coordenadas:')} {fire.lat}, {fire.lon}
- {(fire.type === 'modis' || fire.type === 'viirs') && -{t('Fuego detectado por satélites de la NASA {{when}}', { when: this.dateFromNow })}
+ +{t('Coordenadas:')} {fire.lat}, {fire.lon}
+ {(fire.type === 'modis' || fire.type === 'viirs') && +{t('Fuego notificado por uno de nuestros usuarios/as {{when}}', { when: this.dateFromNow })}
- } - {/* TODO: marcar tipo de fuego, industria, etc */} -
{ this.props.activefires.length === 0 ?
-