More work with subscriptions
This commit is contained in:
parent
918103a17b
commit
497724443f
11 changed files with 164 additions and 62 deletions
|
|
@ -37,7 +37,7 @@ Meteor.publishTransformed('userSubsToFires', function transform() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('subscriptions', function subscriptions() {
|
Meteor.publish('mysubscriptions', function subscriptions() {
|
||||||
return Subscriptions.find({ owner: this.userId });
|
return Subscriptions.find({ owner: this.userId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
const schemaUserProfile = new SimpleSchema({
|
const schemaUserProfile = new SimpleSchema({
|
||||||
name: { type: String, optional: true },
|
// name: { type: String, optional: true },
|
||||||
|
name: Object,
|
||||||
|
'name.first': String,
|
||||||
|
'name.last': String,
|
||||||
lang: { type: String, optional: true },
|
lang: { type: String, optional: true },
|
||||||
telegramChatId: { type: Number, optional: true },
|
telegramChatId: { type: Number, optional: true },
|
||||||
telegramUsername: { type: String, optional: true },
|
telegramUsername: { type: String, optional: true },
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ export const defaultCreatedAt = {
|
||||||
}
|
}
|
||||||
// Prevent user from supplying their own value
|
// Prevent user from supplying their own value
|
||||||
this.unset();
|
this.unset();
|
||||||
}
|
},
|
||||||
|
optional: true // TODO: Use migration for users
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultUpdateAt = {
|
export const defaultUpdateAt = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
/* global setTimeout */
|
/* global setTimeout */
|
||||||
/* eslint-disable react/jsx-indent-props */
|
/* eslint-disable react/jsx-indent-props */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Map, TileLayer, Marker, CircleMarker, Circle } from 'react-leaflet';
|
import { Map, TileLayer, Marker, CircleMarker, Circle } from 'react-leaflet';
|
||||||
import Leaflet from 'leaflet';
|
import Leaflet from 'leaflet';
|
||||||
import { translate } from 'react-i18next';
|
import { Trans, translate } from 'react-i18next';
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
import update from 'immutability-helper';
|
import update from 'immutability-helper';
|
||||||
import geolocation from '/imports/startup/client/geolocation';
|
import geolocation from '/imports/startup/client/geolocation';
|
||||||
|
|
@ -15,6 +17,8 @@ import 'leaflet-graphicscale/dist/Leaflet.GraphicScale.min.js';
|
||||||
import 'leaflet-sleep/Leaflet.Sleep.js';
|
import 'leaflet-sleep/Leaflet.Sleep.js';
|
||||||
import Control from 'react-leaflet-control';
|
import Control from 'react-leaflet-control';
|
||||||
import { Button, ButtonToolbar } from 'react-bootstrap';
|
import { Button, ButtonToolbar } from 'react-bootstrap';
|
||||||
|
import { Sidebar, Tab } from 'react-leaflet-sidebarv2';
|
||||||
|
import '/imports/ui/stylesheets/leaflet-sidebar.min.css';
|
||||||
import './SelectionMap.scss';
|
import './SelectionMap.scss';
|
||||||
|
|
||||||
class SelectionMap extends Component {
|
class SelectionMap extends Component {
|
||||||
|
|
@ -25,7 +29,9 @@ class SelectionMap extends Component {
|
||||||
marker: props.center,
|
marker: props.center,
|
||||||
zoom: 11,
|
zoom: 11,
|
||||||
distance: props.distance,
|
distance: props.distance,
|
||||||
draggable: true
|
draggable: true,
|
||||||
|
sidebarCollapsed: true,
|
||||||
|
sidebarSelected: 'home'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getMap = this.getMap.bind(this);
|
this.getMap = this.getMap.bind(this);
|
||||||
|
|
@ -79,6 +85,7 @@ class SelectionMap extends Component {
|
||||||
this.setState(update(this.state, { $merge: { marker: [lat, lng] } }));
|
this.setState(update(this.state, { $merge: { marker: [lat, lng] } }));
|
||||||
this.props.onSelection({ lat, lng, currentDistance });
|
this.props.onSelection({ lat, lng, currentDistance });
|
||||||
this.fit();
|
this.fit();
|
||||||
|
// this.addScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
fit() {
|
fit() {
|
||||||
|
|
@ -103,62 +110,120 @@ class SelectionMap extends Component {
|
||||||
return this.state.center && this.state.center[0] && this.state.distance;
|
return this.state.center && this.state.center[0] && this.state.distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSidebarClose() {
|
||||||
|
this.setState({ sidebarCollapsed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSidebarOpen(id) {
|
||||||
|
this.setState({
|
||||||
|
sidebarCollapsed: false,
|
||||||
|
sidebarSelected: id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
aTab(oid, name) {
|
||||||
|
return (
|
||||||
|
<Tab id={`sidetab-${oid}`} header={name} icon="fa fa-map-marker">
|
||||||
|
<div className="btn-group-vertical sidebar-tab-btn-group">
|
||||||
|
<Button
|
||||||
|
bsStyle="default"
|
||||||
|
><i className="fa fa-pencil-square-o" />
|
||||||
|
<Trans>Editar</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bsStyle="default"
|
||||||
|
><i className="icons icon-target" />
|
||||||
|
<Trans>Centrar aquí</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bsStyle="default"
|
||||||
|
>
|
||||||
|
<Trans>Desahabilitar</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bsStyle="danger"
|
||||||
|
><i className="fa fa-times" />
|
||||||
|
<Trans>Borrar</Trans>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// console.log('render map called');
|
// console.log('render map called');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ this.isValidState() &&
|
{ this.isValidState() &&
|
||||||
<Map
|
<div className="leaflet-container">
|
||||||
center={this.state.center}
|
<Sidebar
|
||||||
zoom={this.state.zoom}
|
id="sidebar"
|
||||||
ref={(map) => { this.selectionMap = map; }}
|
collapsed={this.state.sidebarCollapsed}
|
||||||
sleep={window.location.pathname === '/'}
|
selected={this.state.sidebarSelected}
|
||||||
sleepTime={10750}
|
closeIcon="fa fa-chevron-left"
|
||||||
wakeTime={750}
|
onOpen={this.onSidebarOpen.bind(this)}
|
||||||
sleepNote
|
onClose={this.onSidebarClose.bind(this)}
|
||||||
hoverToWake
|
>
|
||||||
wakeMessage={this.props.t('Pulsa para activar')}
|
<Tab id="home" header="Suscripciones" icon="fa fa-bars">
|
||||||
sleepOpacity={0.6}
|
<p>Pulsa en alguna de tus suscripciones para editarlas, etc.</p>
|
||||||
>
|
</Tab>
|
||||||
<TileLayer
|
{this.aTab('someid2', 'Some fire')}
|
||||||
attribution="&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
{this.aTab('someid3', 'Some another fire')}
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
</Sidebar>
|
||||||
/>
|
<Map
|
||||||
<Marker
|
className="sidebar-map"
|
||||||
draggable={this.state.draggable}
|
center={this.state.center}
|
||||||
onDragend={this.updatePosition}
|
zoom={this.state.zoom}
|
||||||
position={this.state.marker}
|
ref={(map) => { this.selectionMap = map; }}
|
||||||
icon={positionIcon}
|
sleep={window.location.pathname === '/'}
|
||||||
title={this.props.t('Arrastrar para seleccionar otro punto')}
|
sleepTime={10750}
|
||||||
ref={(ref) => { this.marker = ref; }}
|
wakeTime={750}
|
||||||
/>
|
sleepNote
|
||||||
<CircleMarker
|
hoverToWake
|
||||||
center={this.state.marker}
|
wakeMessage={this.props.t('Pulsa para activar')}
|
||||||
color="red"
|
sleepOpacity={0.6}
|
||||||
stroke={false}
|
>
|
||||||
fillOpacity="1"
|
<TileLayer
|
||||||
fill
|
attribution="&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
||||||
radius={3}
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
<Circle
|
<Marker
|
||||||
center={this.state.marker}
|
draggable={this.state.draggable}
|
||||||
ref={(ref) => { this.distanceCircle = ref; }}
|
onDragend={this.updatePosition}
|
||||||
color="#145A32"
|
position={this.state.marker}
|
||||||
fillColor="green"
|
icon={positionIcon}
|
||||||
fillOpacity={0.1}
|
title={this.props.t('Arrastrar para seleccionar otro punto')}
|
||||||
radius={this.state.distance * 1000}
|
ref={(ref) => { this.marker = ref; }}
|
||||||
/>
|
/>
|
||||||
<Control position="topright" >
|
<CircleMarker
|
||||||
<ButtonToolbar>
|
center={this.state.marker}
|
||||||
<Button
|
color="red"
|
||||||
bsStyle="success"
|
stroke={false}
|
||||||
onClick={event => this.onSubs(event)}
|
fillOpacity="1"
|
||||||
>
|
fill
|
||||||
{this.props.subsBtn}
|
radius={3}
|
||||||
</Button>
|
/>
|
||||||
</ButtonToolbar>
|
<Circle
|
||||||
</Control>
|
center={this.state.marker}
|
||||||
</Map> }
|
ref={(ref) => { this.distanceCircle = ref; }}
|
||||||
|
color="#145A32"
|
||||||
|
fillColor="green"
|
||||||
|
fillOpacity={0.1}
|
||||||
|
radius={this.state.distance * 1000}
|
||||||
|
/>
|
||||||
|
<Control position="topright" >
|
||||||
|
<ButtonToolbar>
|
||||||
|
<Button
|
||||||
|
bsStyle="success"
|
||||||
|
onClick={event => this.onSubs(event)}
|
||||||
|
>
|
||||||
|
{this.props.subsBtn}
|
||||||
|
</Button>
|
||||||
|
</ButtonToolbar>
|
||||||
|
</Control>
|
||||||
|
</Map>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,11 @@
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.leaflet-container {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 400px) {
|
@media only screen and (max-width: 400px) {
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
|
|
@ -21,3 +26,23 @@
|
||||||
.center-in-my-pos {
|
.center-in-my-pos {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
.sidebar-tabs>li>a,
|
||||||
|
.sidebar-tabs>ul>li>a {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-left~.sidebar-map .leaflet-left {
|
||||||
|
left: 315px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-tab-btn-group {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sidebar-tab-btn-group > button > i {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class SubscriptionEditor extends React.Component {
|
||||||
<FireSubscription
|
<FireSubscription
|
||||||
center={[doc.location.lat, doc.location.lon]}
|
center={[doc.location.lat, doc.location.lon]}
|
||||||
distance={doc.distance}
|
distance={doc.distance}
|
||||||
focusInput={!isEdit}
|
focusInput={this.props.focusInput ? this.props.focusInput : !isEdit}
|
||||||
subsBtn={isEdit ? t('Actualizar') : t('Subscribirme a fuegos en este rádio')}
|
subsBtn={isEdit ? t('Actualizar') : t('Subscribirme a fuegos en este rádio')}
|
||||||
onSubs={state => this.onSubs(state)}
|
onSubs={state => this.onSubs(state)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,7 +54,8 @@ SubscriptionEditor.defaultProps = {
|
||||||
SubscriptionEditor.propTypes = {
|
SubscriptionEditor.propTypes = {
|
||||||
doc: PropTypes.object,
|
doc: PropTypes.object,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired
|
history: PropTypes.object.isRequired,
|
||||||
|
focusInput: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate([], { wait: true })(SubscriptionEditor);
|
export default translate([], { wait: true })(SubscriptionEditor);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class Index extends Component {
|
||||||
const fullPageOptions = {
|
const fullPageOptions = {
|
||||||
activeClass: 'active', // the class that is appended to the sections links
|
activeClass: 'active', // the class that is appended to the sections links
|
||||||
anchors: ['home', 'crowdsourcing', 'platforms', 'participe', 'fires'], // the anchors for each sections
|
anchors: ['home', 'crowdsourcing', 'platforms', 'participe', 'fires'], // the anchors for each sections
|
||||||
arrowNavigation: false, // use arrow keys (true after development)
|
arrowNavigation: true, // use arrow keys (true after development)
|
||||||
className: 'section-container', // the class name for the section container
|
className: 'section-container', // the class name for the section container
|
||||||
delay: 1000, // the scroll animation speed
|
delay: 1000, // the scroll animation speed
|
||||||
navigation: true, // use dots navigation
|
navigation: true, // use dots navigation
|
||||||
|
|
@ -219,7 +219,7 @@ class Index extends Component {
|
||||||
<Section className="">
|
<Section className="">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h4 className="page-header"><Trans parent="span">Suscríbete a alertas de fuegos</Trans></h4>
|
<h4 className="page-header"><Trans parent="span">Suscríbete a alertas de fuegos</Trans></h4>
|
||||||
<SubscriptionEditor history={this.props.history} />
|
<SubscriptionEditor history={this.props.history} focusInput={false} />
|
||||||
</div>
|
</div>
|
||||||
<div className="overlay" />
|
<div className="overlay" />
|
||||||
</Section>
|
</Section>
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,10 @@ Subscriptions.propTypes = {
|
||||||
|
|
||||||
|
|
||||||
export default translate([], { wait: true })(withTracker(() => {
|
export default translate([], { wait: true })(withTracker(() => {
|
||||||
const subscription = Meteor.subscribe('subscriptions');
|
const subscription = Meteor.subscribe('mysubscriptions');
|
||||||
// console.log(UserSubsToFiresCollection.find().fetch());
|
// console.log(UserSubsToFiresCollection.find().fetch());
|
||||||
return {
|
return {
|
||||||
loading: !subscription.ready(),
|
loading: !subscription.ready(),
|
||||||
subscriptions: UserSubsToFiresCollection.find().fetch()
|
subscriptions: UserSubsToFiresCollection.find({ owner: Meteor.userId() }).fetch()
|
||||||
};
|
};
|
||||||
})(Subscriptions));
|
})(Subscriptions));
|
||||||
|
|
|
||||||
1
imports/ui/stylesheets/leaflet-sidebar.min.css
vendored
Normal file
1
imports/ui/stylesheets/leaflet-sidebar.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.sidebar{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;z-index:2000;box-shadow:0 1px 5px rgba(0,0,0,.65)}.sidebar.collapsed{width:40px}@media (min-width:768px) and (max-width:991px){.sidebar{width:305px}.sidebar-pane{min-width:265px}}@media (min-width:992px) and (max-width:1199px){.sidebar{width:390px}}@media (min-width:1200px){.sidebar{width:460px}}.sidebar-left{left:0}.sidebar-right{right:0}@media (min-width:768px){.sidebar{top:10px;bottom:10px;transition:width .5s}.sidebar-left{left:10px}.sidebar-right{right:10px}}.sidebar-tabs{top:0;bottom:0;height:100%;background-color:#fff}.sidebar-left .sidebar-tabs{left:0}.sidebar-right .sidebar-tabs{right:0}.sidebar-tabs,.sidebar-tabs>ul{position:absolute;width:40px;margin:0;padding:0}.sidebar-tabs>li,.sidebar-tabs>ul>li{width:100%;height:40px;color:#333;font-size:12pt;overflow:hidden;transition:all 80ms}.sidebar-tabs>li:hover,.sidebar-tabs>ul>li:hover{color:#000;background-color:#eee}.sidebar-tabs>li.active,.sidebar-tabs>ul>li.active{color:#fff;background-color:#0074d9}.sidebar-tabs>li.disabled,.sidebar-tabs>ul>li.disabled{color:rgba(51,51,51,.4)}.sidebar-tabs>li.disabled:hover,.sidebar-tabs>ul>li.disabled:hover{background:0 0}.sidebar-tabs>li.disabled>a,.sidebar-tabs>ul>li.disabled>a{cursor:default}.sidebar-tabs>li>a,.sidebar-tabs>ul>li>a{display:block;width:100%;height:100%;line-height:40px;color:inherit;text-decoration:none;text-align:center}.sidebar-tabs>ul+ul{bottom:0}.sidebar-content{position:absolute;top:0;bottom:0;background-color:rgba(255,255,255,.95);overflow-x:hidden;overflow-y:auto}.sidebar-left .sidebar-content{left:40px;right:0}.sidebar-right .sidebar-content{left:0;right:40px}.sidebar.collapsed>.sidebar-content{overflow-y:hidden}.sidebar-pane{display:none;left:0;right:0;box-sizing:border-box;padding:10px 20px}.sidebar-pane.active{display:block}.sidebar-header{margin:-10px -20px 0;height:40px;padding:0 20px;line-height:40px;font-size:14.4pt;color:#fff;background-color:#0074d9}.sidebar-right .sidebar-header{padding-left:40px}.sidebar-close{position:absolute;top:0;width:40px;height:40px;text-align:center;cursor:pointer}.sidebar-left .sidebar-close{right:0}.sidebar-right .sidebar-close{left:0}.sidebar-left~.sidebar-map{margin-left:40px}.sidebar-right~.sidebar-map{margin-right:40px}.sidebar.leaflet-touch{box-shadow:none;border-right:2px solid rgba(0,0,0,.2)}@media (min-width:768px) and (max-width:991px){.sidebar-left~.sidebar-map .leaflet-left{left:315px}.sidebar-right~.sidebar-map .leaflet-right{right:315px}}@media (min-width:992px) and (max-width:1199px){.sidebar-pane{min-width:350px}.sidebar-left~.sidebar-map .leaflet-left{left:400px}.sidebar-right~.sidebar-map .leaflet-right{right:400px}}@media (min-width:1200px){.sidebar-pane{min-width:420px}.sidebar-left~.sidebar-map .leaflet-left{left:470px}.sidebar-right~.sidebar-map .leaflet-right{right:470px}}@media (min-width:768px){.sidebar-left~.sidebar-map{margin-left:0}.sidebar-right~.sidebar-map{margin-right:0}.sidebar{border-radius:4px}.sidebar.leaflet-touch{border:2px solid rgba(0,0,0,.2)}.sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}.sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px}.sidebar-right~.sidebar-map .leaflet-right{transition:right .5s}.sidebar-right.collapsed~.sidebar-map .leaflet-right{right:50px}}
|
||||||
5
package-lock.json
generated
5
package-lock.json
generated
|
|
@ -9955,6 +9955,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-leaflet-google/-/react-leaflet-google-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-leaflet-google/-/react-leaflet-google-3.2.0.tgz",
|
||||||
"integrity": "sha1-67F5qBIOQ0Ui0GK8eTdojKLtD20="
|
"integrity": "sha1-67F5qBIOQ0Ui0GK8eTdojKLtD20="
|
||||||
},
|
},
|
||||||
|
"react-leaflet-sidebarv2": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-leaflet-sidebarv2/-/react-leaflet-sidebarv2-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-HaEeIq3TfK1Hy7xIuX3hv895LNpu1xIBCgW+I1INrqX/95nNRw6ItNQOxmk0YO5ImrSaDeV6rDXKsAGojjngdg=="
|
||||||
|
},
|
||||||
"react-overlays": {
|
"react-overlays": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.7.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
"react-leaflet": "^1.7.7",
|
"react-leaflet": "^1.7.7",
|
||||||
"react-leaflet-control": "^1.4.0",
|
"react-leaflet-control": "^1.4.0",
|
||||||
"react-leaflet-google": "^3.2.0",
|
"react-leaflet-google": "^3.2.0",
|
||||||
|
"react-leaflet-sidebarv2": "^0.5.1",
|
||||||
"react-places-autocomplete": "^5.4.3",
|
"react-places-autocomplete": "^5.4.3",
|
||||||
"react-resize-detector": "^1.1.0",
|
"react-resize-detector": "^1.1.0",
|
||||||
"react-router-bootstrap": "^0.24.4",
|
"react-router-bootstrap": "^0.24.4",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue