From 316e14f0eaed96120ce8e139f54ac5e218969bfe Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 16 Dec 2017 18:47:08 +0100 Subject: [PATCH] Added subs management (wip) --- .meteor/packages | 1 + .meteor/versions | 1 + imports/api/ActiveFires/ActiveFires.js | 2 +- imports/api/FireAlerts/FireAlerts.js | 11 +- imports/api/Subscriptions/Subscriptions.js | 7 +- imports/api/Subscriptions/methods.js | 53 +++++++++ .../api/Subscriptions/server/publications.js | 10 ++ imports/startup/server/api.js | 2 + .../AuthenticatedNavigation.js | 5 +- .../components/SelectionMap/SelectionMap.js | 27 ++--- .../SubscriptionEditor/SubscriptionEditor.js | 60 ++++++++++ imports/ui/layouts/App/App.js | 13 ++- .../EditSubscription/EditSubscription.js | 33 ++++++ .../FireSubscription/FireSubscription.js | 30 +++-- imports/ui/pages/Index/Index.js | 5 +- .../pages/NewSubscription/NewSubscription.js | 17 +++ .../ui/pages/Subscriptions/Subscriptions.js | 105 ++++++++++++++++++ .../ui/pages/Subscriptions/Subscriptions.scss | 3 + .../ViewSubscription/ViewSubscription.js | 65 +++++++++++ public/locales/en/common.json | 8 +- public/locales/es/common.json | 8 +- 21 files changed, 427 insertions(+), 39 deletions(-) create mode 100644 imports/api/Subscriptions/methods.js create mode 100644 imports/ui/components/SubscriptionEditor/SubscriptionEditor.js create mode 100644 imports/ui/pages/EditSubscription/EditSubscription.js create mode 100644 imports/ui/pages/NewSubscription/NewSubscription.js create mode 100644 imports/ui/pages/Subscriptions/Subscriptions.js create mode 100644 imports/ui/pages/Subscriptions/Subscriptions.scss create mode 100644 imports/ui/pages/ViewSubscription/ViewSubscription.js diff --git a/.meteor/packages b/.meteor/packages index edf1117..c723518 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -48,3 +48,4 @@ ostrio:mailer # mailer ostrio:meteor-root mizzao:user-status babrahams:constellation +percolate:migrations diff --git a/.meteor/versions b/.meteor/versions index e2b7eee..fd46a71 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -104,6 +104,7 @@ peerlibrary:assert@0.2.5 peerlibrary:fiber-utils@0.6.0 peerlibrary:reactive-mongo@0.1.1 peerlibrary:server-autorun@0.5.2 +percolate:migrations@1.0.2 promise@0.10.0 raix:eventemitter@0.1.3 random@1.0.10 diff --git a/imports/api/ActiveFires/ActiveFires.js b/imports/api/ActiveFires/ActiveFires.js index b06da74..789a7b3 100644 --- a/imports/api/ActiveFires/ActiveFires.js +++ b/imports/api/ActiveFires/ActiveFires.js @@ -3,7 +3,7 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -const ActiveFires = new Mongo.Collection('activefires'); +const ActiveFires = new Mongo.Collection('activefires', { idGeneration: 'MONGO' }); ActiveFires.allow({ insert: () => false, diff --git a/imports/api/FireAlerts/FireAlerts.js b/imports/api/FireAlerts/FireAlerts.js index 4eb499b..0652835 100644 --- a/imports/api/FireAlerts/FireAlerts.js +++ b/imports/api/FireAlerts/FireAlerts.js @@ -3,18 +3,18 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -const FireAlerts = new Mongo.Collection('avisosfuego'); +const FireAlerts = new Mongo.Collection('avisosfuego', { idGeneration: 'MONGO' }); FireAlerts.allow({ insert: () => false, update: () => false, - remove: () => false, + remove: () => false }); FireAlerts.deny({ insert: () => true, update: () => true, - remove: () => true, + remove: () => true }); /* Sample: @@ -42,9 +42,8 @@ FireAlerts.schema = new SimpleSchema({ location: Object, 'location.lat': SimpleSchema.Integer, 'location.lon': SimpleSchema.Integer, - aviso: Date, - } -); + aviso: Date +}); FireAlerts.attachSchema(FireAlerts.schema); diff --git a/imports/api/Subscriptions/Subscriptions.js b/imports/api/Subscriptions/Subscriptions.js index aec6bfd..e9e53a9 100644 --- a/imports/api/Subscriptions/Subscriptions.js +++ b/imports/api/Subscriptions/Subscriptions.js @@ -3,7 +3,7 @@ import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; -const Subscriptions = new Mongo.Collection('subscriptions'); +const Subscriptions = new Mongo.Collection('subscriptions', { idGeneration: 'MONGO' }); Subscriptions.allow({ insert: () => false, @@ -41,9 +41,10 @@ Subscriptions.schema = new SimpleSchema({ location: Object, 'location.lat': SimpleSchema.Integer, 'location.lon': SimpleSchema.Integer, - distance: Number + distance: Number, + owner: String }); -Subscriptions.attachSchema(Subscriptions.schema); +// Subscriptions.attachSchema(Subscriptions.schema); export default Subscriptions; diff --git a/imports/api/Subscriptions/methods.js b/imports/api/Subscriptions/methods.js new file mode 100644 index 0000000..e37f8c0 --- /dev/null +++ b/imports/api/Subscriptions/methods.js @@ -0,0 +1,53 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; +import Subscriptions from './Subscriptions'; +import rateLimit from '../../modules/rate-limit'; + +Meteor.methods({ + 'subscriptions.insert': function subscriptionsInsert(doc) { + check(doc, { + location: Match.ObjectIncluding({ lat: Number, lon: Number }), + distance: Number + }); + + try { + return Subscriptions.insert({ owner: this.userId, ...doc }); + } catch (exception) { + throw new Meteor.Error('500', exception); + } + }, + 'subscriptions.update': function subscriptionsUpdate(doc) { + check(doc, { + _id: String, + location: Match.ObjectIncluding({ lat: Number, lon: Number }), + distance: Number + }); + + try { + const subscriptionId = doc._id; + Subscriptions.update(subscriptionId, { $set: doc }); + return subscriptionId; // Return _id so we can redirect to subscription after update. + } catch (exception) { + throw new Meteor.Error('500', exception); + } + }, + 'subscriptions.remove': function subscriptionsRemove(subscriptionId) { + check(subscriptionId, Meteor.Collection.ObjectID); + + try { + return Subscriptions.remove(subscriptionId); + } catch (exception) { + throw new Meteor.Error('500', exception); + } + } +}); + +rateLimit({ + methods: [ + 'subscriptions.insert', + 'subscriptions.update', + 'subscriptions.remove' + ], + limit: 5, + timeRange: 1000 +}); diff --git a/imports/api/Subscriptions/server/publications.js b/imports/api/Subscriptions/server/publications.js index f12ca5b..404cd11 100644 --- a/imports/api/Subscriptions/server/publications.js +++ b/imports/api/Subscriptions/server/publications.js @@ -34,3 +34,13 @@ Meteor.publishTransformed('userSubsToFires', function transform() { return doc; }); }); + +Meteor.publish('subscriptions', function subscriptions() { + return Subscriptions.find({ owner: this.userId }); +}); + +// Note: subscriptions.view is also used when editing an existing subscription. +Meteor.publish('subscriptions.view', function subscriptionsView(subscriptionId) { + check(subscriptionId, String); + return Subscriptions.find({ _id: subscriptionId, owner: this.userId }); +}); diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index 8b392e6..53d263a 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -10,5 +10,7 @@ import '../../api/Utility/server/methods'; import '../../api/ActiveFires/server/publications'; import '../../api/FireAlerts/server/publications'; + +import '../../api/Subscriptions/methods'; import '../../api/Subscriptions/server/publications'; // TODO add rate-limit to these publications diff --git a/imports/ui/components/AuthenticatedNavigation/AuthenticatedNavigation.js b/imports/ui/components/AuthenticatedNavigation/AuthenticatedNavigation.js index 8d10033..c575dbe 100644 --- a/imports/ui/components/AuthenticatedNavigation/AuthenticatedNavigation.js +++ b/imports/ui/components/AuthenticatedNavigation/AuthenticatedNavigation.js @@ -19,6 +19,9 @@ const AuthenticatedNavigation = ({ name, history, props }) => ( Documents + + Subscriptions + {name} @@ -30,7 +33,7 @@ const AuthenticatedNavigation = ({ name, history, props }) => ( ); AuthenticatedNavigation.propTypes = { - name: PropTypes.string.isRequired, + name: PropTypes.string.isRequired }; diff --git a/imports/ui/components/SelectionMap/SelectionMap.js b/imports/ui/components/SelectionMap/SelectionMap.js index df6f154..97c81df 100644 --- a/imports/ui/components/SelectionMap/SelectionMap.js +++ b/imports/ui/components/SelectionMap/SelectionMap.js @@ -33,7 +33,7 @@ class SelectionMap extends Component { this.updatePosition = this.updatePosition.bind(this); this.fit = this.fit.bind(this); this.addScale = this.addScale.bind(this); - this.doSubs = this.doSubs.bind(this); + this.onSubs = this.onSubs.bind(this); } componentDidMount() { @@ -57,6 +57,13 @@ class SelectionMap extends Component { this.fit(); } + onSubs() { + this.props.onSubs({ + location: { lat: this.state.center[0], lon: this.state.center[1] }, + distance: this.state.distance + }); + } + getMap() { return this.selectionMap.leafletElement; } @@ -92,13 +99,6 @@ class SelectionMap extends Component { Leaflet.control.graphicScale([options]).addTo(map); } - doSubs() { // event param not used - this.props.history.push( - '/login', - { subscription: { center: this.state.center, distance: this.state.distance } } - ); - } - isValidState() { return this.state.center && this.state.center[0] && this.state.distance; } @@ -152,9 +152,9 @@ class SelectionMap extends Component { @@ -165,11 +165,12 @@ class SelectionMap extends Component { } SelectionMap.propTypes = { - history: PropTypes.object.isRequired, t: PropTypes.func.isRequired, - center: PropTypes.array, + center: PropTypes.arrayOf(PropTypes.number), distance: PropTypes.number, - onSelection: PropTypes.func.isRequired + onSelection: PropTypes.func.isRequired, + subsBtn: PropTypes.string.isRequired, + onSubs: PropTypes.func.isRequired }; export default translate([], { wait: true })(withTracker(props => ({ diff --git a/imports/ui/components/SubscriptionEditor/SubscriptionEditor.js b/imports/ui/components/SubscriptionEditor/SubscriptionEditor.js new file mode 100644 index 0000000..4d937c1 --- /dev/null +++ b/imports/ui/components/SubscriptionEditor/SubscriptionEditor.js @@ -0,0 +1,60 @@ +/* eslint-disable max-len, no-return-assign */ +/* eslint-disable react/jsx-indent */ +/* eslint-disable react/jsx-indent-props */ +/* eslint-disable import/no-absolute-path */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Meteor } from 'meteor/meteor'; +import { Bert } from 'meteor/themeteorchef:bert'; +import FireSubscription from '/imports/ui/pages/FireSubscription/FireSubscription'; +import { translate } from 'react-i18next'; + +class SubscriptionEditor extends React.Component { + onSubs(value) { + const { t, history } = this.props; + const existingSubscription = this.props.doc && this.props.doc._id; + const methodToCall = existingSubscription ? 'subscriptions.update' : 'subscriptions.insert'; + const doc = { + location: value.location, + distance: value.distance + }; + + if (existingSubscription) doc._id = existingSubscription; + + Meteor.call(methodToCall, doc, (error, subscriptionId) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + const confirmation = existingSubscription ? t('Suscripción actualizada') : t('Suscripción añadida'); + Bert.alert(confirmation, 'success'); + history.push(`/subscriptions/${subscriptionId}`); + } + }); + } + + render() { + const { doc, t } = this.props; + const isEdit = doc && doc._id; + return ( + this.onSubs(state)} + /> + ); + } +} +SubscriptionEditor.defaultProps = { + doc: { location: { lat: null, lot: null }, distance: null } +}; + +SubscriptionEditor.propTypes = { + doc: PropTypes.object, + t: PropTypes.func.isRequired, + history: PropTypes.object.isRequired +}; + +export default translate([], { wait: true })(SubscriptionEditor); diff --git a/imports/ui/layouts/App/App.js b/imports/ui/layouts/App/App.js index 99948e1..e9209ad 100644 --- a/imports/ui/layouts/App/App.js +++ b/imports/ui/layouts/App/App.js @@ -28,6 +28,10 @@ import Documents from '../../pages/Documents/Documents'; import NewDocument from '../../pages/NewDocument/NewDocument'; import ViewDocument from '../../pages/ViewDocument/ViewDocument'; import EditDocument from '../../pages/EditDocument/EditDocument'; +import Subscriptions from '../../pages/Subscriptions/Subscriptions'; +import NewSubscription from '../../pages/NewSubscription/NewSubscription'; +import ViewSubscription from '../../pages/ViewSubscription/ViewSubscription'; +import EditSubscription from '../../pages/EditSubscription/EditSubscription'; import Signup from '../../pages/Signup/Signup'; import Login from '../../pages/Login/Login'; import Logout from '../../pages/Logout/Logout'; @@ -69,13 +73,20 @@ const App = props => ( + + + + + + - } /> + {/* } /> */} + diff --git a/imports/ui/pages/EditSubscription/EditSubscription.js b/imports/ui/pages/EditSubscription/EditSubscription.js new file mode 100644 index 0000000..3923926 --- /dev/null +++ b/imports/ui/pages/EditSubscription/EditSubscription.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { createContainer } from 'meteor/react-meteor-data'; +import { Meteor } from 'meteor/meteor'; +import Subscriptions from '../../../api/Subscriptions/Subscriptions'; +import SubscriptionEditor from '../../components/SubscriptionEditor/SubscriptionEditor'; +import NotFound from '../NotFound/NotFound'; + +const EditSubscription = ({ doc, history }) => (doc ? ( +
+

{`Editing "${doc.title}"`}

+ +
+) : ); + +EditSubscription.defaultProps = { + doc: null +}; + +EditSubscription.propTypes = { + doc: PropTypes.object, + history: PropTypes.object.isRequired +}; + +export default createContainer(({ match }) => { + const subscriptionId = match.params._id; + const subscription = Meteor.subscribe('subscriptions.view', subscriptionId); + + return { + loading: !subscription.ready(), + doc: Subscriptions.findOne(subscriptionId) + }; +}, EditSubscription); diff --git a/imports/ui/pages/FireSubscription/FireSubscription.js b/imports/ui/pages/FireSubscription/FireSubscription.js index 1ce057d..e0f11d5 100644 --- a/imports/ui/pages/FireSubscription/FireSubscription.js +++ b/imports/ui/pages/FireSubscription/FireSubscription.js @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Row, Col } from 'react-bootstrap'; -import { Trans, translate } from 'react-i18next'; +import { translate } from 'react-i18next'; import DistanceSlider from '/imports/ui/components/DistanceSlider/DistanceSlider'; import SelectionMap from '/imports/ui/components/SelectionMap/SelectionMap'; import Gkeys from '/imports/startup/client/Gkeys'; @@ -14,7 +14,9 @@ class FireSubscription extends React.Component { constructor(props) { super(props); this.state = { - init: false + init: false, + center: this.props.center, + distance: this.props.distance }; // console.log(this.props.location.state); } @@ -27,7 +29,7 @@ class FireSubscription extends React.Component { } onAutocompleteChange(value) { - this.setState({ lat: value.lat, lng: value.lng }); + this.setState({ center: [value.lat, value.lng] }); } onSliderChange(value) { @@ -35,11 +37,15 @@ class FireSubscription extends React.Component { } onSelection(value) { - this.setState({ lat: value.lat, lng: value.lng, distance: value.distance }); + this.setState({ center: [value.lat, value.lng], distance: value.distance }); + } + + onSubs(value) { + this.props.onSubs(value); } centerOnUserLocation(value) { - this.setState({ lat: value.center[0], lng: value.center[1] }); + this.setState({ center: value.center }); } render() { @@ -54,7 +60,6 @@ class FireSubscription extends React.Component {
-

Suscríbete a alertas de fuegos

this.onAutocompleteChange(value)} @@ -70,10 +75,11 @@ class FireSubscription extends React.Component { this.onSelection(state)} + onSubs={state => this.onSubs(state)} />
@@ -82,8 +88,12 @@ class FireSubscription extends React.Component { } FireSubscription.propTypes = { - history: PropTypes.object.isRequired, - focusInput: PropTypes.bool.isRequired + t: PropTypes.func.isRequired, + center: PropTypes.arrayOf(PropTypes.number), + distance: PropTypes.number, + focusInput: PropTypes.bool.isRequired, + subsBtn: PropTypes.string.isRequired, + onSubs: PropTypes.func.isRequired }; export default translate([], { wait: true })(FireSubscription); diff --git a/imports/ui/pages/Index/Index.js b/imports/ui/pages/Index/Index.js index 9e1598f..48ff2c6 100644 --- a/imports/ui/pages/Index/Index.js +++ b/imports/ui/pages/Index/Index.js @@ -12,7 +12,7 @@ import ReactResizeDetector from 'react-resize-detector'; import _ from 'lodash'; import { ScrollToTopOnMount, SectionsContainer, Section } from 'react-fullpage'; import 'html5-device-mockups/dist/device-mockups.min.css'; -import FireSubscription from '/imports/ui/pages/FireSubscription/FireSubscription'; +import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor'; import FiresMap from '../FiresMap/FiresMap'; import './Index.scss'; @@ -218,7 +218,8 @@ class Index extends Component {
- +

Suscríbete a alertas de fuegos

+
diff --git a/imports/ui/pages/NewSubscription/NewSubscription.js b/imports/ui/pages/NewSubscription/NewSubscription.js new file mode 100644 index 0000000..564b930 --- /dev/null +++ b/imports/ui/pages/NewSubscription/NewSubscription.js @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Trans, translate } from 'react-i18next'; +import SubscriptionEditor from '../../components/SubscriptionEditor/SubscriptionEditor'; + +const NewSubscription = ({ history }) => ( +
+

Nueva suscripción

+ +
+); + +NewSubscription.propTypes = { + history: PropTypes.object.isRequired +}; + +export default translate([], { wait: true })(NewSubscription); diff --git a/imports/ui/pages/Subscriptions/Subscriptions.js b/imports/ui/pages/Subscriptions/Subscriptions.js new file mode 100644 index 0000000..aed5a70 --- /dev/null +++ b/imports/ui/pages/Subscriptions/Subscriptions.js @@ -0,0 +1,105 @@ +/* eslint-disable react/jsx-indent-props */ +/* eslint-disable import/no-absolute-path */ +/* eslint-disable react/jsx-indent */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { Table, Alert, Button, Row } from 'react-bootstrap'; +import { timeago, monthDayYearAtTime } from '@cleverbeagle/dates'; +import { Meteor } from 'meteor/meteor'; +import { withTracker } from 'meteor/react-meteor-data'; +import { Trans, translate } from 'react-i18next'; +import { Bert } from 'meteor/themeteorchef:bert'; +import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; +import Loading from '../../components/Loading/Loading'; + +import './Subscriptions.scss'; + +const handleRemove = (subscriptionId) => { + if (confirm('Are you sure? This is permanent!')) { + Meteor.call('subscriptions.remove', subscriptionId, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert('Subscription deleted!', 'success'); + } + }); + } +}; + +const Subscriptions = ({ + loading, + subscriptions, + match, + history +}) => (!loading ? ( +
+
+

Suscripciones a fuegos en áreas de mi interés

+ Añadir suscripción +
+
+ {subscriptions.length ? + + + + + + + + + + {subscriptions.map(({ + _id, + location, + createdAt, + updatedAt + }) => ( + + + + + + + + ))} + +
LocationLast UpdatedCreated + +
{location.lat},{location.lon}{timeago(updatedAt)}{monthDayYearAtTime(createdAt)} + + + +
: + Todavía sin suscriptiones + } +
+) : ); + +Subscriptions.propTypes = { + loading: PropTypes.bool.isRequired, + subscriptions: PropTypes.arrayOf(PropTypes.object).isRequired, + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + + +export default translate([], { wait: true })(withTracker(() => { + const subscription = Meteor.subscribe('subscriptions'); + // console.log(UserSubsToFiresCollection.find().fetch()); + return { + loading: !subscription.ready(), + subscriptions: UserSubsToFiresCollection.find().fetch() + }; +})(Subscriptions)); diff --git a/imports/ui/pages/Subscriptions/Subscriptions.scss b/imports/ui/pages/Subscriptions/Subscriptions.scss new file mode 100644 index 0000000..946b9ce --- /dev/null +++ b/imports/ui/pages/Subscriptions/Subscriptions.scss @@ -0,0 +1,3 @@ +.Subscriptions table tbody tr td { + vertical-align: middle; +} diff --git a/imports/ui/pages/ViewSubscription/ViewSubscription.js b/imports/ui/pages/ViewSubscription/ViewSubscription.js new file mode 100644 index 0000000..56e2968 --- /dev/null +++ b/imports/ui/pages/ViewSubscription/ViewSubscription.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ButtonToolbar, ButtonGroup, Button } from 'react-bootstrap'; +import { createContainer } from 'meteor/react-meteor-data'; +import { Meteor } from 'meteor/meteor'; +import { Bert } from 'meteor/themeteorchef:bert'; +import Subscriptions from '../../../api/Subscriptions/Subscriptions'; +import NotFound from '../NotFound/NotFound'; +import Loading from '../../components/Loading/Loading'; + +const handleRemove = (subscriptionId, history) => { + if (confirm('Are you sure? This is permanent!')) { + Meteor.call('subscriptions.remove', subscriptionId, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert('Subscription deleted!', 'success'); + history.push('/subscriptions'); + } + }); + } +}; + +const renderSubscription = (doc, match, history) => (doc ? ( +
+
+

{ doc && doc.title }

+ + + + + + +
+ { doc && doc.body } +
+) : ); + +const ViewSubscription = ({ + loading, + doc, + match, + history +}) => ( + !loading ? renderSubscription(doc, match, history) : +); + +ViewSubscription.propTypes = { + loading: PropTypes.bool.isRequired, + doc: PropTypes.object, + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + +export default createContainer(({ match }) => { + const subscriptionId = match.params._id; + const subscription = Meteor.subscribe('subscriptions.view', subscriptionId); + + return { + loading: !subscription.ready(), + doc: Subscriptions.findOne(subscriptionId) + }; +}, ViewSubscription); diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 499d537..301dc28 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -104,5 +104,11 @@ "Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.": "Images captured by NASA satellites show the smoke of large fires spreading over the Pacific Ocean. Fire activity is outlined in red.", "Colaboración masiva contra los incendios": - "Corwdsourcing against wildfires" + "Corwdsourcing against wildfires", + "Todavía sin subscriptiones": + "No subscriptions yet", + "Suscripción añadida": + "Subscription added", + "Suscripción actualizada": + "Subscription updated" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 8298c69..51da188 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -131,5 +131,11 @@ "Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.": "Imágenes capturadas por los satélites de la NASA muestran el humo de grandes incendios que se extienden sobre el Océano Pacífico. La actividad del fuego está delineada en rojo.", "Colaboración masiva contra los incendios": - "Colaboración masiva contra los incendios" + "Colaboración masiva contra los incendios", + "Todavía sin subscriptiones": + "Todavía sin subscriptiones", + "Suscripción añadida": + "Suscripción añadida", + "Suscripción actualizada": + "Suscripción actualizada" }