mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
Clean master commit
This commit is contained in:
16
src/components/App.jsx
Normal file
16
src/components/App.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import Dashboard from './Dashboard.jsx';
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Dashboard />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
230
src/components/Card.jsx
Normal file
230
src/components/Card.jsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import '../scss/main.scss';
|
||||
import copy from '../js/data/copy.json';
|
||||
import {isNotNullNorUndefined} from '../js/data/utilities';
|
||||
import React from 'react';
|
||||
|
||||
class Card extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFolded: true
|
||||
};
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this.state.isFolded) {
|
||||
this.props.highlight(this.props.event);
|
||||
} else {
|
||||
this.props.highlight();
|
||||
}
|
||||
this.setState({
|
||||
isFolded: !this.state.isFolded
|
||||
});
|
||||
}
|
||||
|
||||
getCategoryColorClass(category) {
|
||||
if (category)
|
||||
return this.props.getCategoryGroup(category);
|
||||
return 'other';
|
||||
}
|
||||
|
||||
renderWarning() {
|
||||
const warning_lang = copy[this.props.language].cardstack.warning;
|
||||
|
||||
if (this.props.event.tags) {
|
||||
const tagsArray = this.props.event.tags.split(",");
|
||||
/* TODO: This needs to be generalized */
|
||||
if (tagsArray.some(tag => {
|
||||
return (tag.name === 'contradicción' ||
|
||||
tag.name === 'declaración con sospecha de tortura')
|
||||
})) {
|
||||
return (<div className="warning event-card-section">{warning_lang}</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderCategory() {
|
||||
const category_lang = copy[this.props.language].cardstack.category;
|
||||
|
||||
const colorType = this.getCategoryColorClass(this.props.event.category);
|
||||
const categoryLabel = this.props.getCategoryLabel(this.props.event.category);
|
||||
|
||||
return (<div className="event-card-section category">
|
||||
<h4>{category_lang}</h4>
|
||||
<p><span className={`color-category ${colorType}`}/>{categoryLabel}</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// NB: is this function for a future feature?
|
||||
renderIncidents() {
|
||||
const incident_type_lang = copy[this.props.language].cardstack.incident_type;
|
||||
const incidentTags = []; //this.props.event.tags.filter(tag => tag.type === 'incident_type');
|
||||
|
||||
return (<div className="event-card-section event-type">
|
||||
<h4>{incident_type_lang}</h4>
|
||||
{
|
||||
incidentTags.map((tag, idx) => {
|
||||
return (<span className={(
|
||||
tag.name === 'contradicción' || tag.name === 'declaración con sospecha de tortura')
|
||||
? ' flagged'
|
||||
: ''}>
|
||||
{tag.name}{
|
||||
(idx < incidentTags.length - 1)
|
||||
? ','
|
||||
: ''
|
||||
}
|
||||
</span>);
|
||||
})
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderSummary() {
|
||||
const summary = copy[this.props.language].cardstack.description;
|
||||
const desc = this.props.event.description;
|
||||
const description = (this.state.isFolded) ? `${desc.substring(0, 40)}...` : desc;
|
||||
return (<div className="event-card-section summary">
|
||||
<h4>{summary}</h4>
|
||||
<p>{description}</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderTags() {
|
||||
const people_lang = copy[this.props.language].cardstack.people;
|
||||
const peopleTags = []; //this.props.event.tags.filter(tag => tag.type === 'people');
|
||||
|
||||
return (<div className="event-card-section tags">
|
||||
<h4>{people_lang}</h4>
|
||||
<p>{
|
||||
peopleTags.map((tag, idx) => {
|
||||
return (<span className="tag">
|
||||
{tag.name}
|
||||
{
|
||||
(idx < peopleTags.length - 1)
|
||||
? ','
|
||||
: ''
|
||||
}
|
||||
</span>);
|
||||
})
|
||||
}</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// NB: is this function for a future feature? Should also be internaionalized.
|
||||
renderLocation() {
|
||||
const location_lang = copy[this.props.language].cardstack.location;
|
||||
if (isNotNullNorUndefined(this.props.event.location)) {
|
||||
return (<p className="event-card-section location">
|
||||
<h4>{location_lang}</h4>
|
||||
<p>{this.props.event.location}</p>
|
||||
</p>);
|
||||
} else {
|
||||
return (<p className="event-card-section location">
|
||||
<h4>{location_lang}</h4>
|
||||
<p>Sin localización conocida.</p>
|
||||
</p>);
|
||||
}
|
||||
}
|
||||
|
||||
renderSource() {
|
||||
const source_lang = copy[this.props.language].cardstack.source;
|
||||
return (<div className="event-card-section source">
|
||||
<h4>{source_lang}</h4>
|
||||
<p>{this.props.event.source}</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
// NB: should be internaionalized.
|
||||
renderTimestamp() {
|
||||
const daytime_lang = copy[this.props.language].cardstack.timestamp;
|
||||
const estimated_lang = copy[this.props.language].cardstack.estimated;
|
||||
|
||||
if (isNotNullNorUndefined(this.props.event.timestamp)) {
|
||||
const timestamp = this.props.tools.parser(this.props.event.timestamp);
|
||||
const timelabel = this.props.tools.formatterWithYear(timestamp);
|
||||
return (<div className="event-card-section timestamp">
|
||||
<h4>{daytime_lang}</h4>
|
||||
{timelabel}
|
||||
</div>);
|
||||
} else {
|
||||
return (<div className="event-card-section timestamp">
|
||||
<h4>{daytime_lang}</h4>
|
||||
Hora no conocida
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
return (<div className="card-collapsed">
|
||||
{this.renderWarning()}
|
||||
{this.renderCategory()}
|
||||
{this.renderTimestamp()}
|
||||
{this.renderSummary()}
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.isFolded) {
|
||||
return (<div className="card-bottomhalf folded"></div>);
|
||||
} else if (this.props.isFetchingEvents) {
|
||||
return (<div className="card-bottomhalf">
|
||||
{this.renderSpinner()}
|
||||
</div>);
|
||||
} else {
|
||||
if (!this.props.event.hasOwnProperty('receiver') && !this.props.event.hasOwnProperty('transmitter')) {
|
||||
return (<div className="card-bottomhalf">
|
||||
{this.renderTimestamp()}
|
||||
{this.renderLocation()}
|
||||
{this.renderTags()}
|
||||
{this.renderSource()}
|
||||
</div>);
|
||||
} else {
|
||||
return (<div className="card-bottomhalf">
|
||||
{this.renderTimestamp()}
|
||||
{this.renderTags()}
|
||||
{this.renderSource()}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderSpinner() {
|
||||
return (<div className="spinner">
|
||||
<div className="double-bounce1"></div>
|
||||
<div className="double-bounce2"></div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
renderArrow() {
|
||||
let classes = (this.state.isFolded)
|
||||
? 'arrow-down folded'
|
||||
: 'arrow-down';
|
||||
return (<div className="card-toggle" onClick={() => this.toggle()}>
|
||||
<p>
|
||||
<i className={classes}></i>
|
||||
</p>
|
||||
</div>);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.isLoading) {
|
||||
return (<li className='event-card'>
|
||||
<div className="card-bottomhalf">
|
||||
{this.renderSpinner()}
|
||||
</div>
|
||||
</li>);
|
||||
} else {
|
||||
return (<li className='event-card'>
|
||||
{this.renderHeader()}
|
||||
{this.renderContent()}
|
||||
{this.renderArrow()}
|
||||
</li>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
||||
99
src/components/CardStack.jsx
Normal file
99
src/components/CardStack.jsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import Card from './Card.jsx';
|
||||
import copy from '../js/data/copy.json';
|
||||
import {
|
||||
isNotNullNorUndefined
|
||||
} from '../js/data/utilities.js';
|
||||
|
||||
class CardStack extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderCards() {
|
||||
if (this.props.selected.length > 0) {
|
||||
return this.props.selected.map((event) => {
|
||||
// if event has property 'name', update with event details
|
||||
const shouldCardUpdate = (event.name);
|
||||
|
||||
return (
|
||||
<Card
|
||||
event={event}
|
||||
shouldCardUpdate={shouldCardUpdate}
|
||||
language={this.props.language}
|
||||
tools={this.props.tools}
|
||||
isFetchingEvents={this.props.isFetchingEvents}
|
||||
getCategoryGroup={this.props.getCategoryGroup}
|
||||
getCategoryGroupColor={this.props.getCategoryGroupColor}
|
||||
getCategoryLabel={this.props.getCategoryLabel}
|
||||
highlight={this.props.highlight}
|
||||
/>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
renderLocation() {
|
||||
let locationName = copy[this.props.language].cardstack.unknown_location;
|
||||
if (this.props.selected.length > 0) {
|
||||
if (isNotNullNorUndefined(this.props.selected[0].location)) {
|
||||
locationName = this.props.selected[0].location;
|
||||
}
|
||||
return (<p className="header-copy">in:<b>{` ${locationName}`}</b></p>)
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const header_lang = copy[this.props.language].cardstack.header;
|
||||
|
||||
if (this.props.isFetchingEvents) {
|
||||
return (
|
||||
<div id="card-stack" className={`card-stack ${this.props.isCardstack ? '' : ' folded'}`}>
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
onClick={() => this.props.toggle('TOGGLE_CARDSTACK')}
|
||||
>
|
||||
<button className="side-menu-burg is-active"><span></span></button>
|
||||
<p className="header-copy top">{copy[this.props.language].loading}</p>
|
||||
</div>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>
|
||||
<Card
|
||||
language={this.props.language}
|
||||
isLoading={true}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.selected.length > 0) {
|
||||
return (
|
||||
<div id="card-stack" className={`card-stack ${this.props.isCardstack ? '' : ' folded'}`}>
|
||||
<div
|
||||
id='card-stack-header'
|
||||
className='card-stack-header'
|
||||
onClick={() => this.props.toggle('TOGGLE_CARDSTACK')}
|
||||
>
|
||||
<button className="side-menu-burg is-active"><span></span></button>
|
||||
<p className="header-copy top">{`${this.props.selected.length} ${header_lang}`}</p>
|
||||
{this.renderLocation()}
|
||||
</div>
|
||||
<div id="card-stack-content" className="card-stack-content">
|
||||
<ul>
|
||||
{this.renderCards()}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default CardStack;
|
||||
11
src/components/Checkbox.jsx
Normal file
11
src/components/Checkbox.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
|
||||
export default ({ label, isActive, onClickLabel, onClickCheckbox }) => (
|
||||
<div className={(isActive) ? 'item active' : 'item'}>
|
||||
<span onClick={() => onClickLabel()}>{label}</span>
|
||||
<button onClick={() => onClickCheckbox()}>
|
||||
<div className="checkbox" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
236
src/components/Dashboard.jsx
Normal file
236
src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import * as selectors from '../selectors';
|
||||
|
||||
import LoadingOverlay from './LoadingOverlay.jsx';
|
||||
import Viewport from './Viewport.jsx';
|
||||
import Toolbar from './Toolbar.jsx';
|
||||
import CardStack from './CardStack.jsx';
|
||||
import InfoPopUp from './InfoPopup.jsx';
|
||||
import Timeline from './Timeline.jsx';
|
||||
import Notification from './Notification.jsx';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleHighlight = this.handleHighlight.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleFilter = this.handleFilter.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.app.isMobile) {
|
||||
this.props.actions.fetchDomain()
|
||||
.then((domain) => this.props.actions.updateDomain(domain));
|
||||
}
|
||||
}
|
||||
|
||||
handleHighlight(highlighted) {
|
||||
this.props.actions.updateHighlighted((highlighted) ? highlighted : null);
|
||||
}
|
||||
|
||||
handleSelect(selected) {
|
||||
if (selected) {
|
||||
// attacks are not susceptible to tag filters, so make sure this happens only when they are found
|
||||
// in the domain
|
||||
let eventsToSelect = selected.map(eventId => this.props.domain.events[eventId]);
|
||||
eventsToSelect = eventsToSelect.sort((a, b) => {
|
||||
return this.props.ui.tools.parser(a.timestamp) - this.props.ui.tools.parser(b.timestamp);
|
||||
});
|
||||
|
||||
if (eventsToSelect.every(event => (event))) {
|
||||
this.props.actions.updateSelected(eventsToSelect);
|
||||
}
|
||||
|
||||
// Now fetch detail data for each event
|
||||
// Add transmitter and receiver data for coevents
|
||||
this.props.actions.fetchEvents(selected)
|
||||
.then((events) => {
|
||||
let eventsSelected = events.map(ev => {
|
||||
return Object.assign({}, ev, this.props.domain.events[ev.id]);
|
||||
});
|
||||
|
||||
eventsSelected = eventsSelected.sort((a, b) => {
|
||||
return this.props.ui.tools.parser(a.timestamp) - this.props.ui.tools.parser(b.timestamp);
|
||||
});
|
||||
|
||||
this.props.actions.updateSelected(eventsSelected);
|
||||
});
|
||||
} else {
|
||||
this.props.actions.updateSelected([]);
|
||||
}
|
||||
}
|
||||
|
||||
handleFilter(filters) {
|
||||
this.props.actions.updateFilters(filters);
|
||||
}
|
||||
|
||||
handleToggle( key ) {
|
||||
switch( key ) {
|
||||
case 'TOGGLE_CARDSTACK': {
|
||||
this.props.actions.updateSelected([]);
|
||||
break;
|
||||
}
|
||||
case 'TOGGLE_INFOPOPUP': {
|
||||
this.props.actions.toggleInfoPopup();
|
||||
break;
|
||||
}
|
||||
case 'TOGGLE_NOTIFICATIONS': {
|
||||
this.props.actions.toggleNotifications();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCategoryGroup(category) {
|
||||
const cat = this.props.domain.categories.find(t => t.category === category)
|
||||
if (cat) return cat.group;
|
||||
return 'other';
|
||||
}
|
||||
|
||||
getCategoryGroupColor(category) {
|
||||
const group = this.getCategoryGroup(category);
|
||||
return this.props.ui.style.groupColors[group];
|
||||
}
|
||||
|
||||
getCategoryLabel(category) {
|
||||
const label = this.props.domain.categories.find(t => t.category === category).category_label;
|
||||
return label;
|
||||
}
|
||||
|
||||
renderTool() {
|
||||
return (<div>
|
||||
<Viewport
|
||||
locations={this.props.domain.locations}
|
||||
sites={this.props.domain.sites}
|
||||
categoryGroups={this.props.domain.categoryGroups}
|
||||
|
||||
views={this.props.app.filters.views}
|
||||
selected={this.props.app.selected}
|
||||
highlighted={this.props.app.highlighted}
|
||||
mapAnchor={this.props.app.mapAnchor}
|
||||
|
||||
uiStyle={this.props.ui.style}
|
||||
dom={this.props.ui.dom}
|
||||
isView2d={this.props.ui.flags.isView2d}
|
||||
|
||||
select={this.handleSelect}
|
||||
highlight={this.handleHighlight}
|
||||
getCategoryGroup={category => this.getCategoryGroup(category)}
|
||||
getCategoryGroupColor={category => this.getCategoryGroupColor(category)}
|
||||
/>
|
||||
<Toolbar
|
||||
tags={this.props.domain.tags}
|
||||
categories={this.props.domain.categories}
|
||||
|
||||
language={this.props.app.language}
|
||||
tagFilters={this.props.app.filters.tags}
|
||||
categoryFilter={this.props.app.filters.categories}
|
||||
viewFilters={this.props.app.filters.views}
|
||||
features={this.props.app.features}
|
||||
|
||||
isToolbar={this.props.ui.flags.isToolbar}
|
||||
toolbarTab={this.props.ui.components.toolbarTab}
|
||||
isView2d={this.props.ui.flags.isView2d}
|
||||
|
||||
filter={this.handleFilter}
|
||||
toggle={ (key) => this.handleToggle(key) }
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<CardStack
|
||||
selected={this.props.app.selected}
|
||||
language={this.props.app.language}
|
||||
|
||||
tools={this.props.ui.tools}
|
||||
isCardstack={this.props.ui.flags.isCardstack}
|
||||
isFetchingEvents={this.props.ui.flags.isFetchingEvents}
|
||||
|
||||
highlight={this.handleHighlight}
|
||||
filter={this.handleFilter}
|
||||
toggle={this.handleToggle}
|
||||
getCategoryGroup={category => this.getCategoryGroup(category)}
|
||||
getCategoryGroupColor={category => this.getCategoryGroupColor(category)}
|
||||
getCategoryLabel={category => this.getCategoryLabel(category)}
|
||||
/>
|
||||
<Timeline
|
||||
events={this.props.domain.events.filter(item => item)}
|
||||
categoryGroups={this.props.domain.categoryGroups}
|
||||
|
||||
range={this.props.app.filters.range}
|
||||
selected={this.props.app.selected}
|
||||
language={this.props.app.language}
|
||||
|
||||
tools={this.props.ui.tools}
|
||||
dom={this.props.ui.dom}
|
||||
|
||||
select={this.handleSelect}
|
||||
filter={this.handleFilter}
|
||||
highlight={this.handleHighlight}
|
||||
toggle={() => this.handleToggle('TOGGLE_CARDSTACK')}
|
||||
getCategoryGroup={category => this.getCategoryGroup(category)}
|
||||
getCategoryGroupColor={category => this.getCategoryGroupColor(category)}
|
||||
getCategoryLabel={category => this.getCategoryLabel(category)}
|
||||
/>
|
||||
<InfoPopUp
|
||||
ui={this.props.ui}
|
||||
app={this.props.app}
|
||||
toggle={() => this.handleToggle('TOGGLE_INFOPOPUP')}
|
||||
/>
|
||||
<Notification
|
||||
isNotification={this.props.ui.flags.isNotification}
|
||||
notifications={this.props.domain.notifications}
|
||||
toggle={() => this.handleToggle('TOGGLE_NOTIFICATIONS')}
|
||||
/>
|
||||
<LoadingOverlay
|
||||
ui={this.props.ui}
|
||||
language={this.props.app.language}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div>{this.renderTool()}</div>);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return Object.assign({}, state, {
|
||||
domain: Object.assign({}, state.domain, {
|
||||
|
||||
events: selectors.getFilteredEvents(state),
|
||||
locations: selectors.getFilteredLocations(state),
|
||||
categories: selectors.getFilteredCategories(state),
|
||||
categoryGroups: selectors.getCategoryGroups(state),
|
||||
sites: selectors.getSites(state),
|
||||
tags: selectors.getTags(state),
|
||||
|
||||
notifications: state.domain.notifications,
|
||||
}),
|
||||
app: Object.assign({}, state.app, {
|
||||
error: state.app.error,
|
||||
filters: Object.assign({}, state.app.filters, {
|
||||
range: selectors.getRangeFilter(state),
|
||||
tags: selectors.getTagFilters(state)
|
||||
})
|
||||
}),
|
||||
ui: state.ui
|
||||
});
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Dashboard);
|
||||
83
src/components/Icon.jsx
Normal file
83
src/components/Icon.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
|
||||
const Icon = ({ iconType }) => {
|
||||
if (iconType === 'personas') {
|
||||
return (
|
||||
<svg x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<path d="M15.464,17.713" />
|
||||
<path d="M5.526,17.713c-1.537,0.595-3,1.472-4.314,2.637l1.114,17.081h16.338" />
|
||||
<path d="M12.283,15.522c-1.707,0.661-3.332,1.636-4.792,2.93l1.238,18.979h18.153" />
|
||||
<circle cx="27.432" cy="8.876" r="6.877" />
|
||||
<path d="M21.297,13.088c-1.896,0.733-3.702,1.817-5.326,3.256l1.375,21.087h20.17l1.376-21.087c-1.624-1.438-3.43-2.522-5.326-3.256" />
|
||||
<path d="M20.968,6.547c-0.926-0.554-2.006-0.877-3.163-0.877c-3.418,0-6.188,2.771-6.188,6.188c0,2.811,1.875,5.18,4.441,5.935" />
|
||||
<path d="M12.38,8.881c-0.738-0.361-1.564-0.57-2.441-0.57c-3.076,0-5.57,2.494-5.57,5.57c0,1.983,1.04,3.72,2.601,4.707" />
|
||||
</svg>
|
||||
);
|
||||
} else if (iconType === 'tipos') {
|
||||
return (
|
||||
<svg x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<path strokeDasharray="3, 4" d="M22.326,5.346
|
||||
c-2.154-2.081-5.082-3.367-8.314-3.367c-6.614,0-11.976,5.361-11.976,11.974c0,6.613,5.361,11.977,11.976,11.977
|
||||
c0.228,0,0.449-0.021,0.674-0.034"/>
|
||||
<circle cx="23" cy="17.288" r="11.975"/>
|
||||
<circle strokeDasharray="3, 4" cx="25.987" cy="26.926" r="11.976" />
|
||||
</svg>
|
||||
);
|
||||
} else if (iconType === 'hardware') {
|
||||
return (
|
||||
<svg x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<path d="M20,1.695C12.571,1.696,6.286,2.019,5.272,2.452C5.253,2.458,5.233,2.466,5.215,2.474
|
||||
c-0.01,0.004-0.019,0.008-0.027,0.012C4.38,2.831,3.803,4.256,3.802,5.907v3.502H2.926H1.175c-0.241,0-0.438,0.196-0.438,0.438
|
||||
v0.875v5.254c0,0.242,0.196,0.438,0.438,0.438h1.751c0.242,0,0.438-0.195,0.438-0.438V11.16h0.438v15.324h5.691
|
||||
c0.242,0,0.438,0.195,0.438,0.438v1.751c0,0.241-0.195,0.438-0.438,0.438H3.802v3.063c0,0.626,0.167,1.203,0.438,1.515v3.74
|
||||
c0,0.482,0.393,0.875,0.876,0.875h2.627c0.483,0,0.875-0.393,0.875-0.875v-2.627h22.765v2.627c0,0.482,0.393,0.875,0.877,0.875
|
||||
h2.627c0.482,0,0.875-0.393,0.875-0.875v-3.74c0.271-0.312,0.438-0.889,0.438-1.515v-3.065h-5.691c-0.241,0-0.438-0.195-0.438-0.438
|
||||
v-1.751c0-0.241,0.197-0.438,0.438-0.438H36.2V11.161h0.438v4.816c0,0.242,0.195,0.438,0.438,0.438h1.752
|
||||
c0.24,0,0.438-0.195,0.438-0.438v-5.254V9.848c0-0.242-0.195-0.438-0.438-0.438h-1.752h-0.875V5.907
|
||||
c-0.001-1.703-0.614-3.159-1.453-3.448C33.79,2.023,27.479,1.696,20,1.695z M5.429,3.28h29.144c0.483,0,0.875,0.98,0.875,2.189l0,0
|
||||
V7.22c0,0.242-0.195,0.438-0.438,0.438H4.991c-0.242,0-0.438-0.196-0.438-0.438V5.469C4.553,4.261,4.945,3.28,5.429,3.28z
|
||||
M5.553,8.534h28.895c0.483,0,0.876,0.392,0.876,0.875v13.134c0,0.484-0.393,0.876-0.876,0.876h-3.466c0,0-0.863,0.613-0.912,0.613
|
||||
H9.931c-0.113,0-0.225-0.022-0.33-0.065l-0.778-0.548h-3.27c-0.483,0-0.875-0.392-0.875-0.876V9.409
|
||||
C4.678,8.926,5.069,8.534,5.553,8.534L5.553,8.534z"/>
|
||||
</svg>
|
||||
);
|
||||
} else if (iconType === 'escenas') {
|
||||
return (
|
||||
<svg className="scenes" x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<path d="M36.729,14.743v13.15l-14.225,6.693V21.438L36.729,14.743 M38.732,11.045L20.5,19.625v18.662l18.232-8.58V11.045
|
||||
L38.732,11.045z" />
|
||||
<path d="M4.271,14.743l14.225,6.695v13.148L4.271,27.894V14.743 M2.268,11.045v18.662l18.232,8.58V19.625L2.268,11.045L2.268,11.045
|
||||
z" />
|
||||
<path d="M20.5,4.844l13.289,6.202L20.5,17.247L7.209,11.046L20.5,4.844 M20.5,2.537L2.268,11.045L20.5,19.554l18.232-8.509
|
||||
L20.5,2.537L20.5,2.537z" />
|
||||
</svg>
|
||||
);
|
||||
} else if (iconType === 'docs') {
|
||||
return (
|
||||
<svg x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<path d="M31.543,5.987V3.158
|
||||
c0-1.103-0.095-1.197-1.197-1.197H4.791c-1.103,0-1.198,0.095-1.198,1.197V32.84c0,1.103,0.095,1.197,1.198,1.197h2.829"/>
|
||||
<path d="M35.57,36.866
|
||||
c0,1.103-0.096,1.198-1.198,1.198H8.817c-1.103,0-1.198-0.096-1.198-1.198V7.185c0-1.103,0.095-1.197,1.198-1.197h25.555
|
||||
c1.103,0,1.198,0.095,1.198,1.197V36.866z"/>
|
||||
<path d="M58.755,29.633"/>
|
||||
<path d="M21.86,40.072"/>
|
||||
<path d="M-22.755,58.555"/>
|
||||
<line x1="11.612" y1="11.977" x2="31.577" y2="11.977"/>
|
||||
<line x1="11.612" y1="17.966" x2="31.577" y2="17.966"/>
|
||||
<line x1="11.612" y1="29.945" x2="31.577" y2="29.945"/>
|
||||
<line x1="11.612" y1="23.955" x2="31.577" y2="23.955"/>
|
||||
</svg>
|
||||
)
|
||||
} else if (iconType === 'search') {
|
||||
return (
|
||||
<svg x="0px" y="0px" width="40px" height="40px" viewBox="0 0 40 40" enableBackground="new 0 0 40 40">
|
||||
<circle cx="18.306" cy="18.307" r="13.856"/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M28.24,28.24
|
||||
l8.346,8.346L28.24,28.24z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Icon;
|
||||
104
src/components/InfoPopup.jsx
Normal file
104
src/components/InfoPopup.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import copy from '../js/data/copy.json';
|
||||
// NB: should we make this componetn part of a future feature?
|
||||
|
||||
export default class InfoPopUp extends React.Component{
|
||||
|
||||
renderView2DCopy() {
|
||||
return copy[this.props.app.language].legend.view2d.paragraphs.map(paragraph => <p>{paragraph}</p>);
|
||||
}
|
||||
|
||||
renderCategoryColors() {
|
||||
const colors = copy[this.props.app.language].legend.view2d.colors.slice(0);
|
||||
colors.reverse();
|
||||
return (
|
||||
<div className="legend-labels" style={{ 'margin-left': '-10px' }}>
|
||||
{colors.map((color, idx) => {
|
||||
return (
|
||||
<div className="label" style={{ 'margin-left': `${idx*5}` }}>
|
||||
<div className={`color-category ${color.class}`}></div>
|
||||
{color.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderView2DLegend() {
|
||||
return (
|
||||
<div className={`infopopup ${(this.props.ui.flags.isInfopopup) ? '' : 'hidden'}`}>
|
||||
<button onClick={() => this.props.toggle()} className="side-menu-burg over-white is-active"><span /></button>
|
||||
{this.renderView2DCopy()}
|
||||
<div className="legend">
|
||||
<div className="legend-section" style={{ 'height': '100px' }}>
|
||||
<svg x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100" enableBackground="new 0 0 100 100">
|
||||
<circle fill="#D2CD28" cx="50" cy="50" r="50"/>
|
||||
<circle fill="#662770" cx="50" cy="50" r="40"/>
|
||||
<circle fill="#2F409A" cx="50" cy="50" r="30"/>
|
||||
<circle fill="#256C36" cx="50" cy="50" r="20"/>
|
||||
<circle fill="#FF0000" cx="50" cy="50" r="10"/>
|
||||
</svg>
|
||||
{this.renderCategoryColors()}
|
||||
</div>
|
||||
<div className="legend-section">
|
||||
<svg x="0px" y="0px" width="100px" height="30px" viewBox="0 0 100 30" enableBackground="new 0 0 100 30">
|
||||
<line fill="none" stroke="#2F409A" strokeDasharray="4,4" x1="30" y1="15" x2="70" y2="15"/>
|
||||
<circle fill="2F409A" fillOpacity="0.2" stroke="#2F409A" strokeDasharray="4,4" cx="80" cy="15" r="10"/>
|
||||
<circle fill="2F409A" fillOpacity="0.2" stroke="#2F409A" strokeDasharray="4,4" cx="20" cy="15" r="10"/>
|
||||
</svg>
|
||||
<div className="legend-labels">
|
||||
<div className="label">Comunicaciones</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="legend-section">
|
||||
<svg x="0px" y="0px" width="100px" height="30px" viewBox="0 0 100 30" enableBackground="new 0 0 100 30">
|
||||
<circle opacity="0.3" fill="#FF0000" cx="50" cy="15" r="15"/>
|
||||
</svg>
|
||||
<div className="legend-labels">
|
||||
<div className="label">Ataques</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="legend-section">
|
||||
<svg x="0px" y="0px" width="100px" height="30px" viewBox="0 40 100 30" enableBackground="new 0 0 100 70">
|
||||
<polyline fill="none" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" stroke-miterlimit="10" points="
|
||||
8.376,63.723 47.287,63.723 60,46 106,46 "/>
|
||||
<line stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" x1="33.723" y1="59.663" x2="39.069" y2="63.723"/>
|
||||
<line stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" x1="33.723" y1="67.782" x2="39.069" y2="63.723"/>
|
||||
<line stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" x1="78.849" y1="41.94" x2="84.195" y2="46"/>
|
||||
<line stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" x1="78.849" y1="50.06" x2="84.195" y2="46"/>
|
||||
</svg>
|
||||
<div className="legend-labels">
|
||||
<div className="label">Rutas de bus</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderView3DLegend() {
|
||||
const lang = copy[this.props.app.language].legend.view3d;
|
||||
return (
|
||||
<div className={`infopopup ${(this.props.ui.flags.isInfopopup) ? '' : 'hidden'}`}>
|
||||
<button onClick={() => this.props.toggle()} className="side-menu-burg over-white is-active"><span /></button>
|
||||
{lang.paragraphs.map(paragraph => <p>{paragraph}</p>)}
|
||||
{lang.colors.map(color => (
|
||||
<div className="legend-item">
|
||||
<div className={`color-marker ${color.class}`}></div>
|
||||
<div className="item-label">{color.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.ui.flags.isView2d) {
|
||||
return (<div>{this.renderView3DLegend()}</div>)
|
||||
}
|
||||
return (
|
||||
<div>{this.renderView2DLegend()}</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
21
src/components/LoadingOverlay.jsx
Normal file
21
src/components/LoadingOverlay.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import copy from '../js/data/copy.json';
|
||||
|
||||
const LoadingOverlay = ({ ui, language }) => {
|
||||
let classes = 'loading-overlay';
|
||||
classes += (!ui.flags.isFetchingDomain) ? ' hidden' : '';
|
||||
|
||||
return (
|
||||
<div id="loading-overlay" className={classes}>
|
||||
<div className="loading-wrapper">
|
||||
<span id="loading-text" className="text">{copy[language].loading}</span>
|
||||
<div className="spinner">
|
||||
<div className="double-bounce1" />
|
||||
<div className="double-bounce2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingOverlay;
|
||||
48
src/components/Notification.jsx
Normal file
48
src/components/Notification.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class Notification extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state = {
|
||||
isExtended: false
|
||||
}
|
||||
}
|
||||
|
||||
toggleDetails() {
|
||||
this.setState({ isExtended: !this.state.isExtended });
|
||||
}
|
||||
|
||||
renderItems(items) {
|
||||
if (!items) return '';
|
||||
return (
|
||||
<div>
|
||||
{items.map((item) => {
|
||||
if (item.error) {
|
||||
return (<p>{item.error.message}</p>);
|
||||
}
|
||||
return '';
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.isNotification) {
|
||||
return (
|
||||
<div className={`notification-wrapper`}>
|
||||
{this.props.notifications.map(not => (
|
||||
<div className='notification' onClick={() => this.toggleDetails() }>
|
||||
<button onClick={() => this.props.toggle()} className="side-menu-burg over-white is-active"><span /></button>
|
||||
<div className={`message ${not.type}`}>{`${not.message}`}</div>
|
||||
<div className={`details ${this.state.isExtended}`}>
|
||||
{(not.items !== null) ? this.renderItems(not.items) : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (<div/>);
|
||||
}
|
||||
}
|
||||
71
src/components/Search.jsx
Normal file
71
src/components/Search.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import copy from '../js/data/copy.json';
|
||||
import TagFilter from './TagFilter.jsx';
|
||||
|
||||
export default class Search extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchValue: undefined,
|
||||
searchResults: []
|
||||
}
|
||||
|
||||
this.handleSearchChange = this.handleSearchChange.bind(this);
|
||||
this.handleSearchSubmit = this.handleSearchSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSearchSubmit(e) {
|
||||
e.preventDefault();
|
||||
fetch(`api/search/${this.state.searchValue}`)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
this.setState({
|
||||
searchResults: json.tags
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
handleSearchChange(event) {
|
||||
this.setState({ searchValue: event.target.value });
|
||||
}
|
||||
|
||||
renderSearchResults() {
|
||||
return (
|
||||
this.state.searchResults.map(tag => {
|
||||
return (
|
||||
<TagFilter
|
||||
isShowTree={true}
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
tag={tag}
|
||||
isCategory={this.props.isCategory}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="search-content">
|
||||
<h2>{copy[this.props.language].toolbar.panels.search.title}</h2>
|
||||
<form onSubmit={this.handleSearchSubmit}>
|
||||
<input
|
||||
value={this.state.searchValue}
|
||||
onChange={this.handleSearchChange}
|
||||
autoFocus
|
||||
type="text"
|
||||
name="search-input"
|
||||
placeholder={copy[this.props.language].toolbar.panels.search.placeholder}
|
||||
/>
|
||||
</form>
|
||||
<ul>
|
||||
{this.renderSearchResults()}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
src/components/TagFilter.jsx
Normal file
91
src/components/TagFilter.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import Checkbox from './Checkbox.jsx';
|
||||
|
||||
class TagFilter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
isActive() {
|
||||
if (this.props.isCategory) {
|
||||
return this.props.categoryFilters.includes(this.props.tag.id);
|
||||
}
|
||||
return this.props.tagFilters.includes(this.props.tag.id);
|
||||
}
|
||||
|
||||
onClickTag() {
|
||||
if (this.isActive()) {
|
||||
this.props.filter({
|
||||
tags: this.props.tagFilters.filter(element => element !== this.props.tag.id)
|
||||
});
|
||||
} else {
|
||||
this.props.filter({
|
||||
tags: this.props.tagFilters.concat(this.props.tag.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickCategory() {
|
||||
if (this.isActive()) {
|
||||
this.props.filter({
|
||||
categories: this.props.categoryFilters.filter(element => element !== this.props.tag.id)
|
||||
});
|
||||
} else {
|
||||
this.props.filter({
|
||||
categories: this.props.categoryFilters.concat(this.props.tag.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderTag() {
|
||||
const tag = this.props.tag;
|
||||
let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter';
|
||||
let label = `${tag.name} ( ${tag.mentions} )`;
|
||||
if (this.props.isShowTree) {
|
||||
label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )`;
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={this.props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={this.isActive()}
|
||||
label={label}
|
||||
onClickLabel={() => this.onClickTag()}
|
||||
onClickCheckbox={() => this.onClickTag()}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renderCategory() {
|
||||
const category = this.props.categories[this.props.tag.id];
|
||||
let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter';
|
||||
|
||||
if (category) {
|
||||
return (
|
||||
<li
|
||||
key={this.props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={this.isActive()}
|
||||
label={`${category.name} ( ${category.counts} )`}
|
||||
onClickLabel={() => this.onClickCategory()}
|
||||
onClickCheckbox={() => this.onClickCategory()}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (<div/>);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.isCategory) return (this.renderCategory());
|
||||
return (this.renderTag());
|
||||
}
|
||||
}
|
||||
|
||||
export default TagFilter;
|
||||
94
src/components/TagListPanel.jsx
Normal file
94
src/components/TagListPanel.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import Checkbox from './Checkbox.jsx';
|
||||
|
||||
class TagListPanel extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
treeComponents: []
|
||||
}
|
||||
this.treeComponents = [];
|
||||
this.newTagFilters = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.computeTree(this.props.tags.children[this.props.tagType]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.computeTree(nextProps.tags.children[nextProps.tagType]);
|
||||
}
|
||||
|
||||
traverseNodeAndCheckIt(node, depth, active) {
|
||||
// do something to node
|
||||
const tagFilter = this.newTagFilters.find(tagFilter => tagFilter.key === node.key)
|
||||
tagFilter.active = (depth === 0) ? !node.active : active;
|
||||
tagFilter.depth = depth;
|
||||
depth = depth + 1;
|
||||
|
||||
if (Object.keys(tagFilter.children).length > 0) {
|
||||
Object.values(tagFilter.children).forEach((childNode) => {
|
||||
this.traverseNodeAndCheckIt(childNode, depth, tagFilters, tagFilter.active);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickCheckbox(tag) {
|
||||
this.newTagFilters = this.props.tagFilters.slice(0);
|
||||
let depth = 0;
|
||||
if (tag.key && tag.children) this.traverseNodeAndCheckIt(tag, depth);
|
||||
|
||||
this.props.filter({ tags: this.newTagFilters });
|
||||
}
|
||||
|
||||
createNodeComponent (node, depth) {
|
||||
return (
|
||||
<li
|
||||
key={node.key.replace(/ /g,"_")}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: `${depth*20}px` }}
|
||||
>
|
||||
<Checkbox
|
||||
label={node.key}
|
||||
isActive={node.active}
|
||||
onClickCheckbox={() => this.onClickCheckbox(node)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
traverseNodeAndCreateComponent(node, depth) {
|
||||
// add and create node component
|
||||
const newComponent = this.createNodeComponent(node, depth);
|
||||
this.treeComponents.push(newComponent)
|
||||
depth = depth + 1;
|
||||
if (Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => {
|
||||
this.traverseNodeAndCreateComponent(childNode, depth);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
computeTree (node) {
|
||||
this.treeComponents = [];
|
||||
let depth = 0;
|
||||
this.traverseNodeAndCreateComponent(node, depth);
|
||||
this.setState({ treeComponents: this.treeComponents });
|
||||
}
|
||||
|
||||
renderTree() {
|
||||
return this.state.treeComponents.map(c => c);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="react-innertabpanel">
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TagListPanel;
|
||||
97
src/components/Timeline.jsx
Normal file
97
src/components/Timeline.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import '../scss/main.scss';
|
||||
import copy from '../js/data/copy.json';
|
||||
import React from 'react';
|
||||
import TimelineLogic from '../js/timeline/timeline.js';
|
||||
|
||||
class Timeline extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isFolded: false};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const domain = {
|
||||
events: this.props.events,
|
||||
categoryGroups: this.props.categoryGroups
|
||||
}
|
||||
const app = {
|
||||
range: this.props.range,
|
||||
selected: this.props.selected,
|
||||
language: this.props.language,
|
||||
select: this.props.select,
|
||||
filter: this.props.filter,
|
||||
getCategoryLabel: this.props.getCategoryLabel,
|
||||
getCategoryGroup: this.props.getCategoryGroup,
|
||||
getCategoryGroupColor: this.props.getCategoryGroupColor
|
||||
}
|
||||
const ui = {
|
||||
tools: this.props.tools,
|
||||
dom: this.props.dom
|
||||
}
|
||||
|
||||
this.timeline = new TimelineLogic(app, ui);
|
||||
this.timeline.update(domain, app);
|
||||
this.timeline.render(domain);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const domain = {
|
||||
events: nextProps.events,
|
||||
categoryGroups: nextProps.categoryGroups
|
||||
}
|
||||
|
||||
const app = {
|
||||
range: nextProps.range,
|
||||
selected: nextProps.selected,
|
||||
language: nextProps.language,
|
||||
select: nextProps.select,
|
||||
filter: nextProps.filter,
|
||||
getCategoryLabel: nextProps.getCategoryLabel,
|
||||
getCategoryGroup: nextProps.getCategoryGroup,
|
||||
getCategoryGroupColor: nextProps.getCategoryGroupColor
|
||||
}
|
||||
|
||||
this.timeline.update(domain, app);
|
||||
this.timeline.render(domain);
|
||||
}
|
||||
|
||||
onClickArrow() {
|
||||
this.setState((prevState, props) => {
|
||||
return {isFolded: !prevState.isFolded};
|
||||
});
|
||||
}
|
||||
|
||||
renderLabels() {
|
||||
const labels = copy[this.props.language].timeline.labels;
|
||||
return this.props.categoryGroups.map((label) => {
|
||||
const groupLen = this.props.categoryGroups.length
|
||||
return (<div className="timeline-label">{label}</div>);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const labels_title_lang = copy[this.props.language].timeline.labels_title;
|
||||
const info_lang = copy[this.props.language].timeline.info;
|
||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
|
||||
const date0 = this.props.tools.formatterWithYear(this.props.range[0]);
|
||||
const date1 = this.props.tools.formatterWithYear(this.props.range[1]);
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="timeline-header">
|
||||
<div className="timeline-toggle" onClick={() => this.onClickArrow()}>
|
||||
<p><i className="arrow-down"></i></p>
|
||||
</div>
|
||||
<div className="timeline-info">
|
||||
<p>{info_lang}</p>
|
||||
<p>{date0} - {date1}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline-content">
|
||||
<div id="timeline" className="timeline" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Timeline;
|
||||
235
src/components/Toolbar.jsx
Normal file
235
src/components/Toolbar.jsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import Search from './Search.jsx';
|
||||
import TagListPanel from './TagListPanel.jsx';
|
||||
import Icon from './Icon.jsx';
|
||||
import copy from '../js/data/copy.json';
|
||||
// NB: i think this entire component can actually be part of a future feature...
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tab: -1
|
||||
};
|
||||
}
|
||||
|
||||
toggleTab(tabIndex) {
|
||||
if ( this.state.tab === tabIndex ) {
|
||||
this.setState({ tab: -1 });
|
||||
} else {
|
||||
this.setState({ tab: tabIndex });
|
||||
}
|
||||
}
|
||||
|
||||
openCabinet() {
|
||||
this.props.actions.openCabinet();
|
||||
}
|
||||
|
||||
resetAllFilters() {
|
||||
this.props.actions.resetAllFilters();
|
||||
}
|
||||
|
||||
toggleInfoPopup() {
|
||||
this.props.actions.toggleInfoPopup();
|
||||
}
|
||||
|
||||
toggleLanguage() {
|
||||
this.props.actions.toggleLanguage();
|
||||
}
|
||||
|
||||
toggleMapViews(layer) {
|
||||
const isLayerInView = !this.props.viewFilters[layer];
|
||||
const newViews = {};
|
||||
newViews[layer] = isLayerInView;
|
||||
const views = Object.assign({}, this.props.viewFilters, newViews);
|
||||
this.props.actions.updateFilters({ views });
|
||||
}
|
||||
|
||||
renderMapActions() {
|
||||
const isViewLayer = this.props.viewFilters;
|
||||
const routeClass = (isViewLayer.routes) ? 'action-button active disabled' : 'action-button disabled'
|
||||
const sitesClass = (isViewLayer.sites) ? 'action-button active disabled' : 'action-button disabled';
|
||||
const coeventsClass = (isViewLayer.coevents) ? 'action-button active disabled' : 'action-button disabled';
|
||||
if (this.props.isView2d) {
|
||||
return (
|
||||
<div className="bottom-action-block">
|
||||
<button
|
||||
className={routeClass}
|
||||
onClick={() => this.toggleMapViews('routes')}
|
||||
>
|
||||
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
|
||||
<path d="M0.806,13.646h7.619c2.762,0,3-0.238,3-3v-0.414c0-2.762,0.301-3,3.246-3h14.523"/>
|
||||
<polyline points="16.671,9.228 19.103,7.233 16.671,5.237 "/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={sitesClass}
|
||||
onClick={() => this.toggleMapViews('sites')}
|
||||
>
|
||||
<svg x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
|
||||
<path d="M24.615,6.793H5.385c-2.761,0-3,0.239-3,3v0.414
|
||||
c0,2.762,0.239,3,3,3h7.621l1.996,2.432l1.996-2.432h7.618c2.762,0,3-0.238,3-3V9.793C27.615,7.032,27.377,6.793,24.615,6.793z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className={coeventsClass}
|
||||
onClick={() => this.toggleMapViews('coevents')}
|
||||
>
|
||||
<svg className="coevents" x="0px" y="0px" width="30px" height="20px" viewBox="0 0 30 20" enableBackground="new 0 0 30 20">
|
||||
<polygon stroke-linejoin="round" stroke-miterlimit="10" points="19.178,20 10.823,20 10.473,14.081
|
||||
10,13.396 10,6.084 20,6.084 20,13.396 19.445,14.021 "/>
|
||||
<rect className="no-fill" x="11.4" y="7.867" width="7.2" height="3.35"/>
|
||||
<line stroke-linejoin="round" stroke-miterlimit="10" x1="12.125" y1="1" x2="12.125" y2="5.35"/>
|
||||
<rect x="11.4" y="4.271" width="1.496" height="1.079"/>
|
||||
<rect x="17.104" y="4.271" width="1.496" height="1.079"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (<div/>)
|
||||
}
|
||||
|
||||
renderBottomActions() {
|
||||
return (
|
||||
<div className="bottom-actions">
|
||||
{this.renderMapActions()}
|
||||
<div className="bottom-action-block">
|
||||
<button className="action-button tiny default" onClick={() => { /*this.toggleLanguage()*/}}>
|
||||
{(this.props.language === 'es-MX') ? 'ES' : 'EN' }
|
||||
</button>
|
||||
<button className="action-button info tiny default" onClick={() => {/*this.toggleInfoPopup()*/}}>
|
||||
i
|
||||
</button>
|
||||
<button className="action-button tiny" onClick={() => this.resetAllFilters()}>
|
||||
<svg className="reset" x="0px" y="0px" width="25px" height="25px" viewBox="7.5 7.5 25 25" enableBackground="new 7.5 7.5 25 25">
|
||||
<path stroke-width="2" stroke-miterlimit="10" d="M28.822,16.386c1.354,3.219,0.898,7.064-1.5,9.924
|
||||
c-3.419,4.073-9.49,4.604-13.562,1.186c-4.073-3.417-4.604-9.49-1.187-13.562c1.987-2.368,4.874-3.54,7.74-3.433" />
|
||||
<polygon points="26.137,12.748 27.621,19.464 28.9,16.741 31.898,16.503" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
renderPanelHeader() {
|
||||
return (
|
||||
<div className="panel-header" onClick={() => this.toggleTab(-1)}>
|
||||
<div className="caret"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderToolbarTab(tabNum, key) {
|
||||
const isActive = (tabNum === this.state.tab);
|
||||
//let caption_lang = copy[this.props.language].toolbar.tabs[tabNum];
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.toggleTab(tabNum); }}>
|
||||
{/*<Icon iconType={key} />*/}
|
||||
<div className="tab-caption">{key}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderToolbarTagRoot() {
|
||||
if (this.props.features.USE_TAGS &&
|
||||
this.props.tags.children) {
|
||||
const roots = Object.values(this.props.tags.children);
|
||||
return roots.map((root, idx) => {
|
||||
return this.renderToolbarTab(idx, root.key);
|
||||
})
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
renderToolbarTabs() {
|
||||
const title = copy[this.props.language].toolbar.title;
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-header" onClick={() => this.openCabinet()}><p>{title}</p></div>
|
||||
<div className="toolbar-tabs">
|
||||
{/*this.renderToolbarTab(0, 'search')*/}
|
||||
{this.renderToolbarTagRoot()}
|
||||
</div>
|
||||
{/* {this.renderBottomActions()} */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderTagListPanel(tagType) {
|
||||
const panels_lang = copy[this.props.language].toolbar.panels;
|
||||
const title = (panels_lang[tagType]) ? panels_lang[tagType].title : tagType;
|
||||
const overview = (panels_lang[tagType]) ? panels_lang[tagType].overview : '';
|
||||
|
||||
return (
|
||||
<TagListPanel
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
title={title}
|
||||
overview={overview}
|
||||
language={this.props.language}
|
||||
tagType={tagType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderSearch() {
|
||||
if (this.props.features.USE_SEARCH) {
|
||||
return (
|
||||
<TabPanel>
|
||||
<Search
|
||||
language={this.props.language}
|
||||
tags={this.props.tags}
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderToolbarTagList() {
|
||||
if (this.props.features.USE_TAGS &&
|
||||
this.props.tags.children) {
|
||||
const roots = Object.values(this.props.tags.children);
|
||||
return roots.map((root, idx) => {
|
||||
return (
|
||||
<TabPanel>
|
||||
{this.renderTagListPanel(root.key)}
|
||||
</TabPanel>
|
||||
)
|
||||
})
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = (this.state.tab !== -1) ? 'toolbar-panels' : 'toolbar-panels folded';
|
||||
|
||||
return (
|
||||
<div id="toolbar-wrapper" className="toolbar-wrapper">
|
||||
{this.renderToolbarTabs()}
|
||||
<div className={classes}>
|
||||
{this.renderPanelHeader()}
|
||||
<Tabs selectedIndex={this.state.tab}>
|
||||
{this.renderToolbarTagList()}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Toolbar;
|
||||
61
src/components/View2D.jsx
Normal file
61
src/components/View2D.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import Map from '../js/map/map.js';
|
||||
import { areEqual } from '../js/data/utilities.js';
|
||||
|
||||
class View2D extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const domain = {
|
||||
locations: this.props.locations,
|
||||
sites: this.props.sites,
|
||||
categoryGroups: this.props.categoryGroups
|
||||
}
|
||||
const app = {
|
||||
views: this.props.views,
|
||||
selected: this.props.selected,
|
||||
highlighted: this.props.highlighted,
|
||||
getCategoryGroup: this.props.getCategoryGroup,
|
||||
getCategoryGroupColor: this.props.getCategoryGroupColor,
|
||||
mapAnchor: this.props.mapAnchor
|
||||
}
|
||||
const ui = {
|
||||
style: this.props.uiStyle,
|
||||
dom: this.props.dom
|
||||
}
|
||||
|
||||
this.map = new Map(app, ui, this.props.select);
|
||||
this.map.update(domain, app);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const domain = {
|
||||
locations: nextProps.locations,
|
||||
sites: nextProps.sites,
|
||||
categoryGroups: nextProps.categoryGroups
|
||||
}
|
||||
const app = {
|
||||
views: nextProps.views,
|
||||
selected: nextProps.selected,
|
||||
highlighted: nextProps.highlighted,
|
||||
getCategoryGroup: nextProps.getCategoryGroup,
|
||||
getCategoryGroupColor: nextProps.getCategoryGroupColor,
|
||||
mapAnchor: this.props.mapAnchor
|
||||
}
|
||||
|
||||
this.map.update(domain, app);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='map-wrapper'>
|
||||
<div id="map" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default View2D;
|
||||
36
src/components/Viewport.jsx
Normal file
36
src/components/Viewport.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import '../scss/main.scss';
|
||||
import React from 'react';
|
||||
import View2D from './View2D.jsx';
|
||||
|
||||
class Viewport extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
if( this.props.isView2d ) {
|
||||
return (
|
||||
<View2D
|
||||
locations={this.props.locations}
|
||||
sites={this.props.sites}
|
||||
categoryGroups={this.props.categoryGroups}
|
||||
|
||||
views={this.props.views}
|
||||
selected={this.props.selected}
|
||||
highlighted={this.props.highlighted}
|
||||
mapAnchor={this.props.mapAnchor}
|
||||
|
||||
uiStyle={this.props.uiStyle}
|
||||
dom={this.props.dom}
|
||||
|
||||
select={this.props.select}
|
||||
highlight={this.props.highlight}
|
||||
getCategoryGroupColor={category => this.props.getCategoryGroupColor(category)}
|
||||
getCategoryGroup={category => this.props.getCategoryGroup(category)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Viewport;
|
||||
Reference in New Issue
Block a user