Clean master commit

This commit is contained in:
Franc FC
2018-10-31 14:11:03 -04:00
parent 59aa005a64
commit 92e03fdb07
69 changed files with 12939 additions and 0 deletions

16
src/components/App.jsx Normal file
View 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
View 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;

View 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;

View 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>
);

View 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
View 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;

View 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>
)
}
}

View 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;

View 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
View 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>
);
}
}

View 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;

View 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;

View 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
View 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
View 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;

View 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;